operations_research/OR_HTML_04/fundament.html

1295 lines
171 KiB
HTML
Raw Normal View History

Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität · Optimierte Entscheidungsfindung mit Python</title>
<script>
(function () {
try {
var t = localStorage.getItem("or-theme");
if (t) document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<link rel="stylesheet" href="assets/highlight.css" />
<link rel="stylesheet" href="katex/katex.min.css" />
<script defer="" src="katex/katex.min.js"></script>
<script>document.addEventListener("DOMContentLoaded", function () {
var mathElements = document.getElementsByClassName("math");
var macros = [];
for (var i = 0; i < mathElements.length; i++) {
var texText = mathElements[i].firstChild;
if (mathElements[i].tagName == "SPAN") {
katex.render(texText.data, mathElements[i], {
displayMode: mathElements[i].classList.contains('display'),
throwOnError: false,
macros: macros,
fleqn: false
});
}}
// Der Browser springt zu einem #anker in der URL schon beim ersten Rendern
// an, BEVOR die KaTeX-Formeln oben im Text ihre finale Hoehe bekommen -
// durch den Reflow landet der Anker danach zu weit unten. Nach dem
// Formel-Rendering hier erneut zum Anker springen, das behebt es.
if (location.hash) {
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
if (ziel) ziel.scrollIntoView({behavior: "instant", block: "start"});
}
});
</script>
<link rel="stylesheet" href="assets/site.css" />
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.2" y2="16.2"/>
</symbol>
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4.5"/>
<line x1="12" y1="1.5" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22.5"/>
<line x1="1.5" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22.5" y2="12"/>
<line x1="4.5" y1="4.5" x2="6.2" y2="6.2"/><line x1="17.8" y1="17.8" x2="19.5" y2="19.5"/>
<line x1="19.5" y1="4.5" x2="17.8" y2="6.2"/><line x1="6.2" y1="17.8" x2="4.5" y2="19.5"/>
</symbol>
<symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
</symbol>
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 4 7 12 15 20"/>
</symbol>
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 4 17 12 9 20"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 13 9.5 18.5 20 6"/>
</symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13.5V19a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19V7A1.5 1.5 0 0 1 5 5.5h5.5"/>
<polyline points="14.5 3.5 20.5 3.5 20.5 9.5"/><line x1="11" y1="13" x2="20" y2="4"/>
</symbol>
<symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5.5A2 2 0 0 1 6 4h6v16H6a2 2 0 0 0-2 1.5z"/>
<path d="M20 5.5A2 2 0 0 0 18 4h-6v16h6a2 2 0 0 1 2 1.5z"/>
</symbol>
<symbol id="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><path d="M4 19.5h16"/>
</symbol></svg>
<header class="site-header">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Menü öffnen"><svg class="icon" aria-hidden="true"><use href="#icon-menu"></use></svg></button>
<a class="brand" href="index.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> <span>Optimierte Entscheidungsfindung mit Python</span></a>
<div class="site-search">
<input id="suche-eingabe" type="search" placeholder="Suchen …" aria-label="Suche" autocomplete="off" />
<svg class="icon such-icon" aria-hidden="true"><use href="#icon-search"></use></svg>
<div id="suche-ergebnisse" class="suche-ergebnisse" hidden></div>
</div>
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Darstellung umschalten">
<svg class="icon icon-sun" aria-hidden="true"><use href="#icon-sun"></use></svg><svg class="icon icon-moon" aria-hidden="true"><use href="#icon-moon"></use></svg>
</button>
</header>
<div class="site-body">
<div class="sidebar-overlay" id="sidebar-overlay" hidden></div>
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen 94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege), F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36 Kapiteldateien 37. Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege auf der neuen Seite, Indexmarken unveraendert bei 328. Das Seitenleisten-Menue bekommt zwei Eintraege: Beispielprogramme Notebooks Download Notebooks als ZIP <- neu, eingerueckt Glossar <- neu Stichwortverzeichnis Gesamtdokument (eine Seite) Download als PDF Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang ist. Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build raeumt alte Seiten nicht ab. Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg, highlight.css und plotly.min.js. --check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise, 328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine Warnung. PDF unveraendert 759 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
<nav class="sidebar" id="sidebar" aria-label="Kapitelnavigation"><div class="sidebar-inhalt"><details class="sidebar-gruppe"><summary>Einstieg</summary><ul><li data-kapitel="vorwort.html"><a href="vorwort.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><li data-kapitel="notation.html"><a href="notation.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><details class="sidebar-gruppe" open><summary>Teil I: Grundlagen des Operations Research</summary><ul><li data-kapitel="einfuehrung.html"><a href="einfuehrung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><li data-kapitel="fundament.html" class="aktiv"><a href="fundament.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><li data-kapitel="oekosystem.html"><a href="oekosystem.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><li data-kapitel="modellierung.html"><a href="modellierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><li data-kapitel="synthese-grundlagen.html"><a href="synthese-grundlagen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><li data-kapitel="lp.html"><a href="lp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><li data-kapitel="milp.html"><a href="milp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><li data-kapitel="cpsat.html"><a href="cpsat.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><li data-kapitel="graphen.html"><a href="graphen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><li data-kapitel="metaheuristiken.html"><a href="metaheuristiken.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><li data-kapitel="dekomposition.html"><a href="dekomposition.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li><li data-kapitel="synthese-kernverfahren.html"><a href="synthese-kernverfahren.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil II</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil III: Nichtlinearität, Unsicherheit
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<main class="content">
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="index.html">Start</a> <span>Teil I</span> <span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></nav>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="einfuehrung.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a><a class="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><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1 id="kap-fundament">Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</h1>
<div class="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> <a href="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>Programme:</strong><br />
<code>Matrixform.py</code><br />
<code>Visualisierung_Loesungsraum.py</code><br />
<code>Konvexitaet_Demo.py</code><br />
<code>Skalierung_Kondition.py</code></p>
Veroeffentlichung: Lizenzen, README, Colab-Entscheidung, JupyterLab im Image Sechs Dinge, alle fuer das jetzt oeffentliche Repository. LIZENZEN. LICENSE traegt die MIT-Lizenz fuer den Programmcode (alle .py, Notebooks, Dockerfile, pyproject.toml), LICENSE-TEXT.md die CC BY-SA 4.0 fuer Buchtext, PDF, Website und Diagramme. Beide Dateien nennen ausdruecklich, was sie abdecken und was nicht - ein Codeblock im Buchtext bleibt Programmcode und steht unter MIT. Vorher hatte das Repository gar keine Lizenz, womit standardmaessig "alle Rechte vorbehalten" galt und niemand die 76 Programme haette weiterverwenden duerfen. COLAB. COLAB_BASIS_URL steht jetzt auf "". Der Platzhalter zeigte auf GitHub, das Repository liegt auf einer eigenen Forgejo-Instanz - und Colab oeffnet Notebooks NUR aus GitHub, Google Drive oder einem Upload. Die URL-Form colab.research.google.com/github/... ist fest auf GitHub verdrahtet; eine selbstgehostete Adresse dort einzusetzen ergaebe keinen Link zum eigenen Server, sondern einen toten GitHub-Link. Der Kommentar im Quelltext ging von GitHub aus und war damit selbst irrefuehrend; er ist ersetzt. Verloren geht nichts: Die 25 Notebooks liegen neben der Website und bekommen einen echten Download-Link, jetzt mit dem Hinweis, was man damit tut - "herunterladen und in Jupyter oeffnen, in Colab hochladen oder mit dem Kurs-Image starten". JUPYTERLAB IM KURS-IMAGE. Neue pyproject-Gruppe [notebook] mit jupyterlab, die Notebooks werden ins Image kopiert, und ein kleiner Startbefehl macht beide Betriebsarten ohne --entrypoint moeglich: ohne Argument JupyterLab, mit Argument ein einzelnes Programm. Gebaut und geprueft - Rucksack.py laeuft, JupyterLab antwortet mit HTTP 200 und zeigt alle 25 Notebooks. Image 1,31 -> 1,46 GB. Es laeuft ohne Token, deshalb im README die Portfreigabe an 127.0.0.1 gebunden. README KOMPLETT NEU. Es war die Bau-Anleitung eines privaten Verzeichnisses und ist jetzt die Visitenkarte eines oeffentlichen Repositorys: was das Buch ist, wo man es liest, drei Wege die Beispiele auszufuehren (Container, schlanke Installation, alles auf einmal), was hier liegt, wie man baut, die Colab-Frage, die Lizenzen und wie man mitwirkt. Alle relativen Links geprueft: 0 tot. .gitattributes. Ohne die Datei entschied core.autocrlf des jeweiligen Rechners, was beim Klonen passiert - ein Windows-Leser bekam CRLF-Rauschen in jedem Diff. Jetzt: im Repository immer LF, im Arbeitsverzeichnis passend zum System, Binaerdateien ausdruecklich ausgenommen. Der Bestand war bereits sauber (git add --renormalize aendert null Dateien). Zusaetzlich sind die erzeugten Verzeichnisse als linguist-generated markiert, sonst zaehlt die Sprachstatistik das Repository als HTML-Projekt. PROGRESS.md: Remote-Repository als erledigt markiert, der Colab-Befund festgehalten. CLAUDE.md um Veroeffentlichung, Lizenzen und die neuen Dateien ergaenzt. Geprueft: --check ohne Fehler, 0 tote README-Links, 76 Programme unveraendert, 33 pytest-Tests, pyproject baut mit acht Gruppen, PDF 759 Seiten (eine weniger - die Colab-Zeile entfaellt in 25 Kapiteln). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:54:04 +02:00
<p><strong>Notebook:</strong> <a href="Notebooks_04/fundament.ipynb">fundament.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
</blockquote>
</div>
<hr />
<h2 id="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>
<div class="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><span class="math display">\max\ 3x_1 + 5x_2 \quad \text{u. d. N.}\quad x_1 + x_2 \le 8,\quad 2x_1 + x_2 \le 12,\quad x_1, x_2 \ge 0</span></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>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> itertools</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>A <span class="op">=</span> np.array([[<span class="dv">1</span>, <span class="dv">1</span>], [<span class="dv">2</span>, <span class="dv">1</span>], [<span class="op">-</span><span class="dv">1</span>, <span class="dv">0</span>], [<span class="dv">0</span>, <span class="op">-</span><span class="dv">1</span>]]) <span class="co"># letzte zwei Zeilen: -x &lt;= 0</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>b <span class="op">=</span> np.array([<span class="dv">8</span>, <span class="dv">12</span>, <span class="dv">0</span>, <span class="dv">0</span>])</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>c <span class="op">=</span> np.array([<span class="dv">3</span>, <span class="dv">5</span>])</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> i, j <span class="kw">in</span> itertools.combinations(<span class="bu">range</span>(<span class="dv">4</span>), <span class="dv">2</span>):</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a> <span class="cf">try</span>:</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a> ecke <span class="op">=</span> np.linalg.solve(A[[i, j]], b[[i, j]]) <span class="co"># Schnittpunkt zweier Geraden</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a> <span class="cf">except</span> np.linalg.LinAlgError:</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">continue</span> <span class="co"># Geraden parallel: keine Ecke</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a> ecke[np.<span class="bu">abs</span>(ecke) <span class="op">&lt;</span> <span class="fl">1e-12</span>] <span class="op">=</span> <span class="fl">0.0</span> <span class="co"># -0.0 aufräumen</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> np.<span class="bu">all</span>(A <span class="op">@</span> ecke <span class="op">&lt;=</span> b <span class="op">+</span> <span class="fl">1e-9</span>): <span class="co"># liegt die Ecke im Bereich?</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Ecke (</span><span class="sc">{</span>ecke[<span class="dv">0</span>]<span class="sc">:.1f}</span><span class="ss">, </span><span class="sc">{</span>ecke[<span class="dv">1</span>]<span class="sc">:.1f}</span><span class="ss">) -&gt; Z = </span><span class="sc">{</span>c <span class="op">@</span> ecke<span class="sc">:6.2f}</span><span class="ss">&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Ecke (4.0, 4.0) -&gt; Z = 32.00
Ecke (0.0, 8.0) -&gt; Z = 40.00
Ecke (6.0, 0.0) -&gt; Z = 18.00
Ecke (0.0, 0.0) -&gt; Z = 0.00</code></pre>
</blockquote>
</div>
<p><strong>Das Optimum lautet <span class="math inline">(0, 8)</span> mit <span class="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 <a href="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 <span class="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>
</blockquote>
<hr />
<h2 id="sec:fundament-lernziele">2.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="1">
<li>… ein Optimierungsmodell von der „Zeile-für-Zeile“-Schreibweise in die kompakte Matrixform <span class="math inline">\max \mathbf{c}^\top\mathbf{x}</span> u. d. N. <span class="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> <span class="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 />
<h2 id="sec:fundament-warum-ueberhaupt-vektoren-und-matrizen">2.3 Warum überhaupt Vektoren und Matrizen?</h2>
<p>In <a href="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>
<h3 id="die-bausteine">Die Bausteine</h3>
<p>Sei <span class="math inline">n</span> die Anzahl der Entscheidungsvariablen und <span class="math inline">m</span> die Anzahl der Nebenbedingungen.</p>
<p><strong>Entscheidungsvektor</strong> — was wir festlegen:</p>
<p><span class="math display">
\mathbf{x} = \begin{pmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{pmatrix} \in \mathbb{R}^n
</span></p>
<p><strong>Kosten- bzw. Ertragsvektor</strong> — was jede Einheit wert ist:</p>
<p><span class="math display">
\mathbf{c} = \begin{pmatrix} c_1 \\ c_2 \\ \vdots \\ c_n \end{pmatrix} \in \mathbb{R}^n
</span></p>
<p><strong>Zielfunktion als Skalarprodukt:</strong></p>
<p><span class="math display">
f(\mathbf{x}) = \mathbf{c}^\top \mathbf{x} = \sum_{j=1}^n c_j x_j
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> * <span class="math inline">\mathbf{c}^\top</span> — der Ertragsvektor, „umgekippt“ zu einer Zeile. * <span class="math inline">\mathbf{c}^\top \mathbf{x}</span><strong>Skalarprodukt</strong>: Multipliziere jedes <span class="math inline">c_j</span> mit dem zugehörigen <span class="math inline">x_j</span> und addiere alles. Das Ergebnis ist <strong>eine einzige Zahl</strong>. * <span class="math inline">\sum_{j=1}^n</span> — „addiere für <span class="math inline">j = 1</span> bis <span class="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: <span class="math inline">\mathbf{c} = (150, 250)^\top</span>, <span class="math inline">\mathbf{x} = (7, 5)^\top</span>, also <span class="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><span class="math display">
\mathbf{A} = \begin{pmatrix}
a_{11} &amp; a_{12} &amp; \cdots &amp; a_{1n} \\
a_{21} &amp; a_{22} &amp; \cdots &amp; a_{2n} \\
\vdots &amp; \vdots &amp; \ddots &amp; \vdots \\
a_{m1} &amp; a_{m2} &amp; \cdots &amp; 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 <span class="math inline">a_{ij}</span> beantwortet: „Wie viel von Ressource <span class="math inline">i</span> verbraucht eine Einheit von Variable <span class="math inline">j</span>?“</p>
<p>Im Bot-Beispiel: <span class="math display">\mathbf{A} = \begin{pmatrix} 2 &amp; 5 \\ 4 &amp; 6 \\ 1 &amp; 0 \end{pmatrix} \begin{matrix} \leftarrow \text{vCPU} \\ \leftarrow \text{RAM} \\ \leftarrow \text{Liquidität} \end{matrix}</span> Die erste Zeile <span class="math inline">(2, 5)</span> heißt: Ein A-Bot braucht 2 vCPUs, ein B-Bot braucht 5. Die dritte Zeile <span class="math inline">(1, 0)</span> heißt: Die Liquiditätsgrenze zählt nur A-Bots, B-Bots gar nicht.</p>
</blockquote>
<p><strong>Kapazitätsvektor (rechte Seite)</strong>:</p>
<p><span class="math display">
\mathbf{b} = \begin{pmatrix} b_1 \\ b_2 \\ \vdots \\ b_m \end{pmatrix} \in \mathbb{R}^m
\qquad \text{im Beispiel } \begin{pmatrix} 40 \\ 60 \\ 8 \end{pmatrix}
</span></p>
<h3 id="die-kanonische-standardform">Die kanonische Standardform</h3>
<p>Damit lässt sich <strong>jedes</strong> lineare Programm in drei Zeilen schreiben:</p>
<p><span class="math display">
\min_{\mathbf{x}} \; \mathbf{c}^\top \mathbf{x}
\qquad \text{u. d. N.} \qquad
\mathbf{A}\mathbf{x} \le \mathbf{b}, \qquad \mathbf{x} \ge \mathbf{0}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> * <span class="math inline">\mathbf{A}\mathbf{x}</span><strong>Matrix-Vektor-Produkt</strong>: berechnet auf einen Schlag den Verbrauch <em>aller</em> Ressourcen. Zeile <span class="math inline">i</span> des Ergebnisses ist <span class="math inline">a_{i1}x_1 + a_{i2}x_2 + \dots + a_{in}x_n</span>. * <span class="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span> — die Ungleichung gilt <strong>komponentenweise</strong>: jede einzelne Zeile muss ihre Kapazität einhalten. * <span class="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: <span class="math inline">\max\ \mathbf{c}^\top\mathbf{x}</span> ist dasselbe wie <span class="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 <a href="lp.html#sec:lp-die-vorzeichenfalle-bei-schattenpreisen">Abschnitt 5.7</a>.</li>
<li><strong><span class="math inline">\ge</span>“-Bedingungen direkt einsetzen.</strong> Die Standardform kennt nur „<span class="math inline">\le</span>“. Aus <span class="math inline">3x_1 + 2x_2 \ge 12</span> wird durch Multiplikation mit <span class="math inline">-1</span>: <span class="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> <span class="math inline">h(\mathbf{x}) = b</span> lässt sich als zwei Ungleichungen schreiben: <span class="math inline">h(\mathbf{x}) \le b</span> <strong>und</strong> <span class="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>Gegeben: <span class="math display">\max\ 3x_1 + 5x_2 \quad \text{u. d. N.}\quad x_1 \le 4,\quad 2x_2 \le 12,\quad 3x_1 + 2x_2 \le 18,\quad x_1, x_2 \ge 0</span></p>
<p>Schritt 1 — Vektoren und Matrix ablesen: <span class="math display">\mathbf{c} = \begin{pmatrix}3\\5\end{pmatrix},\quad \mathbf{A} = \begin{pmatrix}1 &amp; 0\\ 0 &amp; 2\\ 3 &amp; 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 <span class="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 <span class="math inline">\mathbf{A}</span>.</p>
<p>Schritt 2 — Für die Standardform (Minimierung) negieren: <span class="math inline">\tilde{\mathbf{c}} = (-3, -5)^\top</span>. Der optimale Zielwert der Minimierung ist dann das Negative des gesuchten Maximums.</p>
<p>Schritt 3 — Probe mit <span class="math inline">\mathbf{x} = (2,6)^\top</span>: <span class="math display">\mathbf{A}\mathbf{x} = \begin{pmatrix}1\cdot2 + 0\cdot6\\ 0\cdot2+2\cdot6\\ 3\cdot2+2\cdot6\end{pmatrix} = \begin{pmatrix}2\\12\\18\end{pmatrix} \le \begin{pmatrix}4\\12\\18\end{pmatrix} \ \checkmark</span> Alle drei Zeilen halten. Zielwert: <span class="math inline">3\cdot2 + 5\cdot6 = 36</span>.</p>
</blockquote>
<h3 id="dasselbe-in-python">Dasselbe in Python</h3>
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Matrixform.py</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Fundament: Von der ausgeschriebenen Form zur Matrixform - und zurück.</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="co">Zeigt, dass beide Schreibweisen dasselbe Modell beschreiben.</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Modell in Matrixform -------------------------------------------------</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="co"># max 3*x1 + 5*x2 u.d.N. x1 &lt;= 4, 2*x2 &lt;= 12, 3*x1 + 2*x2 &lt;= 18, x &gt;= 0</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>c <span class="op">=</span> np.array([<span class="fl">3.0</span>, <span class="fl">5.0</span>]) <span class="co"># Ertragsvektor (Maximierung)</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>A <span class="op">=</span> np.array([[<span class="fl">1.0</span>, <span class="fl">0.0</span>], <span class="co"># Zeile 1: nur x1 kommt vor</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> [<span class="fl">0.0</span>, <span class="fl">2.0</span>], <span class="co"># Zeile 2: nur x2 kommt vor</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> [<span class="fl">3.0</span>, <span class="fl">2.0</span>]]) <span class="co"># Zeile 3: beide</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>b <span class="op">=</span> np.array([<span class="fl">4.0</span>, <span class="fl">12.0</span>, <span class="fl">18.0</span>])</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>namen <span class="op">=</span> [<span class="st">&quot;Rohstoff A&quot;</span>, <span class="st">&quot;Rohstoff B&quot;</span>, <span class="st">&quot;Maschinenzeit&quot;</span>]</span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Ausgeschriebene Form maschinell erzeugen ------------------------------</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige_ausgeschrieben(c, A, b, namen):</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Druckt die Matrixform als lesbares Ungleichungssystem.&quot;&quot;&quot;</span></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> terme <span class="op">=</span> <span class="st">&quot; + &quot;</span>.join(<span class="ss">f&quot;</span><span class="sc">{</span>c[j]<span class="sc">:g}</span><span class="ss">*x</span><span class="sc">{</span>j<span class="op">+</span><span class="dv">1</span><span class="sc">}</span><span class="ss">&quot;</span> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(c)))</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;max </span><span class="sc">{</span>terme<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;u.d.N.&quot;</span>)</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(A.shape[<span class="dv">0</span>]):</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> summanden <span class="op">=</span> <span class="st">&quot; + &quot;</span>.join(<span class="ss">f&quot;</span><span class="sc">{</span>A[i, j]<span class="sc">:g}</span><span class="ss">*x</span><span class="sc">{</span>j<span class="op">+</span><span class="dv">1</span><span class="sc">}</span><span class="ss">&quot;</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(A.shape[<span class="dv">1</span>]) <span class="cf">if</span> A[i, j] <span class="op">!=</span> <span class="dv">0</span>)</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>summanden<span class="sc">:&lt;24}</span><span class="ss"> &lt;= </span><span class="sc">{</span>b[i]<span class="sc">:&gt;5g}</span><span class="ss"> (</span><span class="sc">{</span>namen[i]<span class="sc">}</span><span class="ss">)&quot;</span>)</span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; x1, ..., x</span><span class="sc">{</span><span class="bu">len</span>(c)<span class="sc">}</span><span class="ss"> &gt;= 0&quot;</span>)</span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a>zeige_ausgeschrieben(c, A, b, namen)</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Zulässigkeit eines Punktes prüfen ------------------------------------</span></span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> ist_zulaessig(x, A, b, toleranz<span class="op">=</span><span class="fl">1e-9</span>):</span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Prüft A x &lt;= b und x &gt;= 0 komponentenweise.&quot;&quot;&quot;</span></span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> verbrauch <span class="op">=</span> A <span class="op">@</span> x <span class="co"># Matrix-Vektor-Produkt: alle Zeilen auf einmal</span></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">bool</span>(np.<span class="bu">all</span>(verbrauch <span class="op">&lt;=</span> b <span class="op">+</span> toleranz) <span class="kw">and</span> np.<span class="bu">all</span>(x <span class="op">&gt;=</span> <span class="op">-</span>toleranz))</span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> kandidat <span class="kw">in</span> [np.array([<span class="fl">2.0</span>, <span class="fl">6.0</span>]), np.array([<span class="fl">4.0</span>, <span class="fl">3.0</span>]), np.array([<span class="fl">4.0</span>, <span class="fl">6.0</span>])]:</span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> zulaessig <span class="op">=</span> ist_zulaessig(kandidat, A, b)</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a> zielwert <span class="op">=</span> c <span class="op">@</span> kandidat</span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a> verbrauch <span class="op">=</span> A <span class="op">@</span> kandidat</span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">x = </span><span class="sc">{</span>kandidat<span class="sc">}</span><span class="ss"> -&gt; A x = </span><span class="sc">{</span>verbrauch<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;zulaessig&#39;</span> <span class="cf">if</span> zulaessig <span class="cf">else</span> <span class="st">&#39;UNZULAESSIG&#39;</span><span class="sc">}</span><span class="ss">, Z = </span><span class="sc">{</span>zielwert<span class="sc">:g}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Lösen: linprog minimiert, also c negieren -----------------------------</span></span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a>ergebnis <span class="op">=</span> linprog(c<span class="op">=-</span>c, A_ub<span class="op">=</span>A, b_ub<span class="op">=</span>b, bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> <span class="bu">len</span>(c), method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb3-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">60</span>)</span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Optimale Loesung: x* = </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(ergebnis.x, <span class="dv">4</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Optimaler Wert: Z* = </span><span class="sc">{</span><span class="op">-</span>ergebnis<span class="sc">.</span>fun<span class="sc">:g}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;Hinweis: linprog minimiert, deshalb wurde c negiert und das&quot;</span>)</span>
<span id="cb3-54"><a href="#cb3-54" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot; Ergebnis am Ende wieder mit -1 multipliziert.&quot;</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>max 3*x1 + 5*x2
u.d.N.
1*x1 &lt;= 4 (Rohstoff A)
2*x2 &lt;= 12 (Rohstoff B)
3*x1 + 2*x2 &lt;= 18 (Maschinenzeit)
x1, ..., x2 &gt;= 0
x = [2. 6.] -&gt; A x = [ 2. 12. 18.] zulaessig, Z = 36
x = [4. 3.] -&gt; A x = [ 4. 6. 18.] zulaessig, Z = 27
x = [4. 6.] -&gt; A x = [ 4. 12. 24.] UNZULAESSIG, Z = 42
------------------------------------------------------------
Optimale Loesung: x* = [2. 6.]
Optimaler Wert: Z* = 36
Hinweis: linprog minimiert, deshalb wurde c negiert und das
Ergebnis am Ende wieder mit -1 multipliziert.</code></pre>
<p>Der dritte Kandidat <span class="math inline">(4, 6)</span> zeigt die Falle: Er hätte mit <span class="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 />
<h2 id="sec:fundament-der-zulaessige-loesungsraum-und-das-polyeder">2.4 Der zulässige Lösungsraum und das Polyeder</h2>
<p>Jede lineare Ungleichung <span class="math inline">a_{i1}x_1 + a_{i2}x_2 \le b_i</span> definiert geometrisch eine <strong>Halbebene</strong> (im <span class="math inline">\mathbb{R}^2</span>) bzw. einen <strong>Halbraum</strong> (im <span class="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 <span class="math inline">\mathcal{F}</span>:</p>
<p><span class="math display">
\mathcal{F} = \{ \mathbf{x} \in \mathbb{R}^n \mid \mathbf{A}\mathbf{x} \le \mathbf{b},\; \mathbf{x} \ge \mathbf{0} \}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> Die geschweiften Klammern beschreiben eine <strong>Menge</strong>. Der senkrechte Strich <span class="math inline">\mid</span> heißt „für die gilt“.</p>
<p><strong>Ohne Formel gesagt:</strong><span class="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 <span class="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>
<img src="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." />
<figcaption aria-hidden="true">Abb. 2.1: Der zulässige Bereich des Bot-Allokationsproblems aus <a href="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>
<h3 id="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 <span class="math inline">\mathcal{F}</span>.</p>
</blockquote>
<p><strong>Warum das gilt — die Anschauung:</strong> Die Zielfunktion <span class="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 (<a href="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>Wir nehmen wieder <span class="math inline">\max 3x_1 + 5x_2</span> mit <span class="math inline">x_1 \le 4</span>, <span class="math inline">2x_2 \le 12</span>, <span class="math inline">3x_1 + 2x_2 \le 18</span>, <span class="math inline">x_1, x_2 \ge 0</span>.</p>
<p><strong>Schritt 1 — Ecken bestimmen.</strong> Eine Ecke entsteht dort, wo sich zwei Begrenzungsgeraden schneiden (und der Punkt zulässig ist). Die Geraden sind: <span class="math inline">x_1 = 0</span>, <span class="math inline">x_2 = 0</span>, <span class="math inline">x_1 = 4</span>, <span class="math inline">x_2 = 6</span>, <span class="math inline">3x_1 + 2x_2 = 18</span>.</p>
<table>
<thead>
<tr class="header">
<th>Schnittpunkt von</th>
<th>Ecke</th>
<th>zulässig?</th>
<th><span class="math inline">Z = 3x_1 + 5x_2</span></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">x_1=0</span>, <span class="math inline">x_2=0</span></td>
<td><span class="math inline">(0,0)</span></td>
<td></td>
<td>0</td>
</tr>
<tr class="even">
<td><span class="math inline">x_1=4</span>, <span class="math inline">x_2=0</span></td>
<td><span class="math inline">(4,0)</span></td>
<td></td>
<td>12</td>
</tr>
<tr class="odd">
<td><span class="math inline">x_1=4</span>, <span class="math inline">3x_1+2x_2=18</span></td>
<td><span class="math inline">(4,3)</span></td>
<td></td>
<td>27</td>
</tr>
<tr class="even">
<td><span class="math inline">x_2=6</span>, <span class="math inline">3x_1+2x_2=18</span></td>
<td><span class="math inline">(2,6)</span></td>
<td></td>
<td><strong>36</strong> ← Optimum</td>
</tr>
<tr class="odd">
<td><span class="math inline">x_1=0</span>, <span class="math inline">x_2=6</span></td>
<td><span class="math inline">(0,6)</span></td>
<td></td>
<td>30</td>
</tr>
<tr class="even">
<td><span class="math inline">x_1=4</span>, <span class="math inline">x_2=6</span></td>
<td><span class="math inline">(4,6)</span></td>
<td>✗ (<span class="math inline">3\cdot4+2\cdot6=24&gt;18</span>)</td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Schritt 2 — Ergebnis ablesen:</strong> <span class="math inline">\mathbf{x}^* = (2,6)^\top</span>, <span class="math inline">Z^* = 36</span>.</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 <span class="math inline">n = 2</span> geht das von Hand; bei <span class="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 />
<h2 id="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>
<figure>
<img src="bilder_04/kap02_konvexitaet.svg" alt="Abb. 2.2: Konvexe vs. nicht-konvexe Zielfunktion" />
<figcaption aria-hidden="true">Abb. 2.2: Konvexe vs. nicht-konvexe Zielfunktion</figcaption>
</figure>
<h3 id="konvexe-menge">Konvexe Menge</h3>
<p>Eine Menge <span class="math inline">\mathcal{C} \subseteq \mathbb{R}^n</span> heißt <strong>konvex</strong>, wenn für alle Punkte <span class="math inline">\mathbf{x}, \mathbf{y} \in \mathcal{C}</span> und jedes <span class="math inline">\theta \in [0,1]</span> gilt:</p>
<p><span class="math display">
\theta \mathbf{x} + (1-\theta)\mathbf{y} \in \mathcal{C}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> * <span class="math inline">\theta</span> („theta“) ist eine Zahl zwischen 0 und 1. * <span class="math inline">\theta\mathbf{x} + (1-\theta)\mathbf{y}</span> durchläuft für <span class="math inline">\theta</span> von 1 bis 0 genau die <strong>Verbindungsstrecke</strong> von <span class="math inline">\mathbf{x}</span> nach <span class="math inline">\mathbf{y}</span>. Bei <span class="math inline">\theta=1</span> sind wir in <span class="math inline">\mathbf{x}</span>, bei <span class="math inline">\theta=0</span> in <span class="math inline">\mathbf{y}</span>, bei <span class="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>
<h3 id="konvexe-funktion">Konvexe Funktion</h3>
<p>Eine Funktion <span class="math inline">f: \mathcal{C} \to \mathbb{R}</span> heißt <strong>konvex</strong>, wenn für alle <span class="math inline">\mathbf{x}, \mathbf{y} \in \mathcal{C}</span> und <span class="math inline">\theta \in [0,1]</span> gilt:</p>
<p><span class="math display">
f(\theta\mathbf{x} + (1-\theta)\mathbf{y}) \le \theta f(\mathbf{x}) + (1-\theta)f(\mathbf{y})
</span></p>
<blockquote>
<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 <span class="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>
<h3 id="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>
</blockquote>
<h3 id="was-ist-konvex-was-nicht">Was ist konvex, was nicht?</h3>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Problemtyp</th>
<th>Konvex?</th>
<th>Konsequenz</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Lineares Programm (LP)</td>
<td><strong>ja</strong></td>
<td>Globales Optimum, Sekunden bis Minuten selbst bei <span class="math inline">10^6</span> Variablen</td>
</tr>
<tr class="even">
<td>Quadratisches Programm mit <span class="math inline">\mathbf{P} \succeq 0</span></td>
<td><strong>ja</strong></td>
<td>Globales Optimum, effizient (<a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a>, <a href="markowitz.html#kap-markowitz">Kapitel 19</a>)</td>
</tr>
<tr class="odd">
<td>Ganzzahlige Variablen (MILP)</td>
<td><strong>nein</strong></td>
<td>Der zulässige Bereich ist ein Punktgitter, keine zusammenhängende Menge — NP-schwer (<a href="milp.html#kap-milp">Kapitel 6</a>)</td>
</tr>
<tr class="even">
<td>Quadratisches Programm mit indefinitem <span class="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 <a href="qp-nlp.html#sec:qp-nlp-nichtlineare-optimierung-mit-scipy-optimize-minimize">Abschnitt 11.5</a>)</td>
</tr>
<tr class="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>
</tr>
</tbody>
</table>
<h3 id="konvexität-sichtbar-machen">Konvexität sichtbar machen</h3>
<div class="sourceCode" id="cb5"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Konvexitaet_Demo.py</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Fundament: Konvexität praktisch erfahrbar machen.</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="co">Teil 1: Die Sehnen-Bedingung numerisch nachprüfen.</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="co">Teil 2: Zeigen, warum lokale Suche bei nicht-konvexen Funktionen scheitert.</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> minimize</span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Teil 1: Sehnen-Test ---------------------------------------------------</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> ist_konvex_numerisch(f, unten, oben, proben<span class="op">=</span><span class="dv">2000</span>, seed<span class="op">=</span><span class="dv">0</span>):</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a><span class="co"> Prüft die Konvexitätsdefinition an zufälligen Punktepaaren:</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a><span class="co"> f(theta*x + (1-theta)*y) &lt;= theta*f(x) + (1-theta)*f(y) ?</span></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a><span class="co"> Findet Gegenbeispiele - beweist aber KEINE Konvexität.</span></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(seed)</span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> schlimmste_verletzung <span class="op">=</span> <span class="fl">0.0</span></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(proben):</span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a> x, y <span class="op">=</span> rng.uniform(unten, oben, size<span class="op">=</span><span class="dv">2</span>)</span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a> theta <span class="op">=</span> rng.uniform(<span class="fl">0.0</span>, <span class="fl">1.0</span>)</span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a> links <span class="op">=</span> f(theta <span class="op">*</span> x <span class="op">+</span> (<span class="dv">1</span> <span class="op">-</span> theta) <span class="op">*</span> y)</span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a> rechts <span class="op">=</span> theta <span class="op">*</span> f(x) <span class="op">+</span> (<span class="dv">1</span> <span class="op">-</span> theta) <span class="op">*</span> f(y)</span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a> schlimmste_verletzung <span class="op">=</span> <span class="bu">max</span>(schlimmste_verletzung, links <span class="op">-</span> rechts)</span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> schlimmste_verletzung</span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a>funktionen <span class="op">=</span> {</span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;f(x) = x^2 (konvex)&quot;</span>: <span class="kw">lambda</span> x: x <span class="op">**</span> <span class="dv">2</span>,</span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;f(x) = |x| (konvex)&quot;</span>: <span class="kw">lambda</span> x: <span class="bu">abs</span>(x),</span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;f(x) = e^x (konvex)&quot;</span>: <span class="kw">lambda</span> x: np.exp(x),</span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;f(x) = x^3 (NICHT konvex)&quot;</span>: <span class="kw">lambda</span> x: x <span class="op">**</span> <span class="dv">3</span>,</span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;f(x) = x^2+3sin(3x) (NICHT konvex)&quot;</span>: <span class="kw">lambda</span> x: x <span class="op">**</span> <span class="dv">2</span> <span class="op">+</span> <span class="dv">3</span> <span class="op">*</span> np.sin(<span class="dv">3</span> <span class="op">*</span> x),</span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">68</span>)</span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot; TEIL 1: SEHNEN-TEST (positive Zahl = Konvexität verletzt)&quot;</span>)</span>
<span id="cb5-41"><a href="#cb5-41" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">68</span>)</span>
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> name, f <span class="kw">in</span> funktionen.items():</span>
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a> verletzung <span class="op">=</span> ist_konvex_numerisch(f, <span class="op">-</span><span class="fl">3.0</span>, <span class="fl">3.0</span>)</span>
<span id="cb5-44"><a href="#cb5-44" aria-hidden="true" tabindex="-1"></a> urteil <span class="op">=</span> <span class="st">&quot;konvex (keine Verletzung gefunden)&quot;</span> <span class="cf">if</span> verletzung <span class="op">&lt;</span> <span class="fl">1e-9</span> <span class="op">\</span></span>
<span id="cb5-45"><a href="#cb5-45" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span> <span class="ss">f&quot;NICHT konvex (Verletzung bis </span><span class="sc">{</span>verletzung<span class="sc">:.3f}</span><span class="ss">)&quot;</span></span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;34}</span><span class="ss"> -&gt; </span><span class="sc">{</span>urteil<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-47"><a href="#cb5-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Teil 2: Lokale Suche von verschiedenen Startpunkten -------------------</span></span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> wellige_funktion(x):</span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Nicht-konvex: eine Parabel mit aufmodulierter Welle -&gt; viele lokale Minima.&quot;&quot;&quot;</span></span>
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> x[<span class="dv">0</span>] <span class="op">**</span> <span class="dv">2</span> <span class="op">+</span> <span class="fl">3.0</span> <span class="op">*</span> np.sin(<span class="fl">3.0</span> <span class="op">*</span> x[<span class="dv">0</span>])</span>
<span id="cb5-52"><a href="#cb5-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-53"><a href="#cb5-53" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">68</span>)</span>
<span id="cb5-54"><a href="#cb5-54" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot; TEIL 2: LOKALE SUCHE BEI NICHT-KONVEXER FUNKTION&quot;</span>)</span>
<span id="cb5-55"><a href="#cb5-55" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">68</span>)</span>
<span id="cb5-56"><a href="#cb5-56" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Startpunkt&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;gefundenes Minimum&#39;</span><span class="sc">:&gt;20}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Funktionswert&#39;</span><span class="sc">:&gt;14}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-57"><a href="#cb5-57" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">68</span>)</span>
<span id="cb5-58"><a href="#cb5-58" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-59"><a href="#cb5-59" aria-hidden="true" tabindex="-1"></a>ergebnisse <span class="op">=</span> []</span>
<span id="cb5-60"><a href="#cb5-60" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> start <span class="kw">in</span> [<span class="op">-</span><span class="fl">3.0</span>, <span class="op">-</span><span class="fl">1.5</span>, <span class="fl">0.0</span>, <span class="fl">1.5</span>, <span class="fl">3.0</span>]:</span>
<span id="cb5-61"><a href="#cb5-61" aria-hidden="true" tabindex="-1"></a> res <span class="op">=</span> minimize(wellige_funktion, x0<span class="op">=</span>[start], method<span class="op">=</span><span class="st">&quot;BFGS&quot;</span>)</span>
<span id="cb5-62"><a href="#cb5-62" aria-hidden="true" tabindex="-1"></a> ergebnisse.append((start, res.x[<span class="dv">0</span>], res.fun))</span>
<span id="cb5-63"><a href="#cb5-63" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>start<span class="sc">:&gt;12.1f}</span><span class="ss"> | </span><span class="sc">{</span>res<span class="sc">.</span>x[<span class="dv">0</span>]<span class="sc">:&gt;20.4f}</span><span class="ss"> | </span><span class="sc">{</span>res<span class="sc">.</span>fun<span class="sc">:&gt;14.4f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-64"><a href="#cb5-64" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-65"><a href="#cb5-65" aria-hidden="true" tabindex="-1"></a>bester <span class="op">=</span> <span class="bu">min</span>(ergebnisse, key<span class="op">=</span><span class="kw">lambda</span> t: t[<span class="dv">2</span>])</span>
<span id="cb5-66"><a href="#cb5-66" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">68</span>)</span>
<span id="cb5-67"><a href="#cb5-67" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Je nach Startpunkt landet derselbe Algorithmus in &quot;</span></span>
<span id="cb5-68"><a href="#cb5-68" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">len</span>({<span class="bu">round</span>(e[<span class="dv">1</span>], <span class="dv">3</span>) <span class="cf">for</span> e <span class="kw">in</span> ergebnisse})<span class="sc">}</span><span class="ss"> verschiedenen Minima.&quot;</span>)</span>
<span id="cb5-69"><a href="#cb5-69" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Das beste gefundene: x = </span><span class="sc">{</span>bester[<span class="dv">1</span>]<span class="sc">:.4f}</span><span class="ss"> mit f = </span><span class="sc">{</span>bester[<span class="dv">2</span>]<span class="sc">:.4f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-70"><a href="#cb5-70" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(Start bei </span><span class="sc">{</span>bester[<span class="dv">0</span>]<span class="sc">:.1f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb5-71"><a href="#cb5-71" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;Bei einer KONVEXEN Funktion waeren alle Zeilen identisch --&quot;</span>)</span>
<span id="cb5-72"><a href="#cb5-72" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;der Startpunkt waere voellig gleichgueltig.&quot;</span>)</span>
<span id="cb5-73"><a href="#cb5-73" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">68</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>====================================================================
TEIL 1: SEHNEN-TEST (positive Zahl = Konvexität verletzt)
====================================================================
f(x) = x^2 (konvex) -&gt; konvex (keine Verletzung gefunden)
f(x) = |x| (konvex) -&gt; konvex (keine Verletzung gefunden)
f(x) = e^x (konvex) -&gt; konvex (keine Verletzung gefunden)
f(x) = x^3 (NICHT konvex) -&gt; NICHT konvex (Verletzung bis 12.933)
f(x) = x^2+3sin(3x) (NICHT konvex) -&gt; NICHT konvex (Verletzung bis 4.800)
====================================================================
TEIL 2: LOKALE SUCHE BEI NICHT-KONVEXER FUNKTION
====================================================================
Startpunkt | gefundenes Minimum | Funktionswert
--------------------------------------------------------------------
-3.0 | -2.4280 | 3.3695
-1.5 | -0.4874 | -2.7448
0.0 | -0.4874 | -2.7448
1.5 | 1.4606 | -0.7042
3.0 | 3.3817 | 9.4570
--------------------------------------------------------------------
Je nach Startpunkt landet derselbe Algorithmus in 4 verschiedenen Minima.
Das beste gefundene: x = -0.4874 mit f = -2.7448 (Start bei -1.5)
Bei einer KONVEXEN Funktion waeren alle Zeilen identisch --
der Startpunkt waere voellig gleichgueltig.
====================================================================</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
<th>Warum wichtig</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>ist_konvex_numerisch</code></td>
<td>testet die Definition an Zufallspaaren</td>
<td>Ein einziges Gegenbeispiel <strong>widerlegt</strong> Konvexität; kein noch so großer Test <strong>beweist</strong> sie</td>
</tr>
<tr class="even">
<td><code>links - rechts</code></td>
<td>Abstand zur Sehne</td>
<td>Positiv = Graph liegt über der Sehne = Verletzung</td>
</tr>
<tr class="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>
<tr class="even">
<td><code>method="BFGS"</code></td>
<td>klassisches lokales Gradientenverfahren</td>
<td>Genau der Verfahrenstyp, den <code>scipy.optimize</code> in <a href="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 <span class="math inline">-3{,}0</span> aus landet das Verfahren bei <span class="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> <span class="math inline">f(x) = x^2</span> ist nichtlinear, aber konvex und damit bequem lösbar. <span class="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 <span class="math inline">\ge 0</span> sind, ist <span class="math inline">\mathbf{x}^\top\mathbf{P}\mathbf{x}</span> konvex. Ein einziger negativer Eigenwert kippt das ganze Problem — mehr dazu in <a href="qp-nlp.html#sec:qp-nlp-nichtlineare-optimierung-mit-scipy-optimize-minimize">Abschnitt 11.5</a>.</li>
</ul>
</blockquote>
<hr />
<h2 id="sec:fundament-geometrische-visualisierung-des-loesungsraums">2.6 Geometrische Visualisierung des Lösungsraums</h2>
<p>Wir zeichnen nun den Lösungsraum aus <a href="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>
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Visualisierung_Loesungsraum.py</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Fundament: Geometrische Visualisierung eines 2D-Optimierungsraums.</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a><span class="co">Ecken werden berechnet, auf Zulässigkeit geprüft, bewertet und eingezeichnet.</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="co">Zusätzlich wird das ganzzahlige Optimum systematisch bestimmt statt behauptet.</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> itertools</span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> os</span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> matplotlib</span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a>matplotlib.use(<span class="st">&quot;Agg&quot;</span>) <span class="co"># kein Bildschirm nötig</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> matplotlib.pyplot <span class="im">as</span> plt</span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a>OUTPUT_DIR <span class="op">=</span> os.path.join(os.path.dirname(os.path.abspath(<span class="va">__file__</span>)), <span class="st">&quot;output&quot;</span>)</span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a>os.makedirs(OUTPUT_DIR, exist_ok<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Modell (identisch zum Kapitel Einfuehrung) ----------------------------</span></span>
<span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a><span class="co"># max 150*xA + 250*xB</span></span>
<span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a><span class="co"># u.d.N. 2*xA + 5*xB &lt;= 40 (vCPU)</span></span>
<span id="cb7-25"><a href="#cb7-25" aria-hidden="true" tabindex="-1"></a><span class="co"># 4*xA + 6*xB &lt;= 60 (RAM)</span></span>
<span id="cb7-26"><a href="#cb7-26" aria-hidden="true" tabindex="-1"></a><span class="co"># 1*xA + 0*xB &lt;= 8 (Marktliquidität)</span></span>
<span id="cb7-27"><a href="#cb7-27" aria-hidden="true" tabindex="-1"></a>c <span class="op">=</span> np.array([<span class="fl">150.0</span>, <span class="fl">250.0</span>])</span>
<span id="cb7-28"><a href="#cb7-28" aria-hidden="true" tabindex="-1"></a>A <span class="op">=</span> np.array([[<span class="fl">2.0</span>, <span class="fl">5.0</span>], [<span class="fl">4.0</span>, <span class="fl">6.0</span>], [<span class="fl">1.0</span>, <span class="fl">0.0</span>]])</span>
<span id="cb7-29"><a href="#cb7-29" aria-hidden="true" tabindex="-1"></a>b <span class="op">=</span> np.array([<span class="fl">40.0</span>, <span class="fl">60.0</span>, <span class="fl">8.0</span>])</span>
<span id="cb7-30"><a href="#cb7-30" aria-hidden="true" tabindex="-1"></a>restriktionsnamen <span class="op">=</span> [<span class="st">&quot;vCPU&quot;</span>, <span class="st">&quot;RAM&quot;</span>, <span class="st">&quot;Marktlimit&quot;</span>]</span>
<span id="cb7-31"><a href="#cb7-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-32"><a href="#cb7-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-33"><a href="#cb7-33" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> ist_zulaessig(punkt, tol<span class="op">=</span><span class="fl">1e-7</span>):</span>
<span id="cb7-34"><a href="#cb7-34" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> np.<span class="bu">all</span>(A <span class="op">@</span> punkt <span class="op">&lt;=</span> b <span class="op">+</span> tol) <span class="kw">and</span> np.<span class="bu">all</span>(punkt <span class="op">&gt;=</span> <span class="op">-</span>tol)</span>
<span id="cb7-35"><a href="#cb7-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-36"><a href="#cb7-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-37"><a href="#cb7-37" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> berechne_ecken():</span>
<span id="cb7-38"><a href="#cb7-38" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-39"><a href="#cb7-39" aria-hidden="true" tabindex="-1"></a><span class="co"> Ecken = Schnittpunkte je zweier Begrenzungsgeraden, die zulässig sind.</span></span>
<span id="cb7-40"><a href="#cb7-40" aria-hidden="true" tabindex="-1"></a><span class="co"> Begrenzungen sind die 3 Restriktionen plus die beiden Achsen xA=0, xB=0.</span></span>
<span id="cb7-41"><a href="#cb7-41" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-42"><a href="#cb7-42" aria-hidden="true" tabindex="-1"></a> geraden <span class="op">=</span> [(A[i], b[i]) <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(b))]</span>
<span id="cb7-43"><a href="#cb7-43" aria-hidden="true" tabindex="-1"></a> geraden.append((np.array([<span class="fl">1.0</span>, <span class="fl">0.0</span>]), <span class="fl">0.0</span>)) <span class="co"># xA = 0</span></span>
<span id="cb7-44"><a href="#cb7-44" aria-hidden="true" tabindex="-1"></a> geraden.append((np.array([<span class="fl">0.0</span>, <span class="fl">1.0</span>]), <span class="fl">0.0</span>)) <span class="co"># xB = 0</span></span>
<span id="cb7-45"><a href="#cb7-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-46"><a href="#cb7-46" aria-hidden="true" tabindex="-1"></a> ecken <span class="op">=</span> []</span>
<span id="cb7-47"><a href="#cb7-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> (n1, d1), (n2, d2) <span class="kw">in</span> itertools.combinations(geraden, <span class="dv">2</span>):</span>
<span id="cb7-48"><a href="#cb7-48" aria-hidden="true" tabindex="-1"></a> M <span class="op">=</span> np.array([n1, n2])</span>
<span id="cb7-49"><a href="#cb7-49" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(np.linalg.det(M)) <span class="op">&lt;</span> <span class="fl">1e-9</span>: <span class="co"># parallel -&gt; kein Schnittpunkt</span></span>
<span id="cb7-50"><a href="#cb7-50" aria-hidden="true" tabindex="-1"></a> <span class="cf">continue</span></span>
<span id="cb7-51"><a href="#cb7-51" aria-hidden="true" tabindex="-1"></a> p <span class="op">=</span> np.linalg.solve(M, np.array([d1, d2]))</span>
<span id="cb7-52"><a href="#cb7-52" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> ist_zulaessig(p) <span class="kw">and</span> <span class="kw">not</span> <span class="bu">any</span>(np.allclose(p, e) <span class="cf">for</span> e <span class="kw">in</span> ecken):</span>
<span id="cb7-53"><a href="#cb7-53" aria-hidden="true" tabindex="-1"></a> ecken.append(p)</span>
<span id="cb7-54"><a href="#cb7-54" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> np.array(ecken)</span>
<span id="cb7-55"><a href="#cb7-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-56"><a href="#cb7-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-57"><a href="#cb7-57" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> bestes_ganzzahliges():</span>
<span id="cb7-58"><a href="#cb7-58" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Vollständige Suche über das kleine Gitter - hier zulässig, weil winzig.&quot;&quot;&quot;</span></span>
<span id="cb7-59"><a href="#cb7-59" aria-hidden="true" tabindex="-1"></a> bester_wert, bester_punkt <span class="op">=</span> <span class="op">-</span>np.inf, <span class="va">None</span></span>
<span id="cb7-60"><a href="#cb7-60" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> xa <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">0</span>, <span class="dv">21</span>):</span>
<span id="cb7-61"><a href="#cb7-61" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> xb <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">0</span>, <span class="dv">21</span>):</span>
<span id="cb7-62"><a href="#cb7-62" aria-hidden="true" tabindex="-1"></a> p <span class="op">=</span> np.array([<span class="bu">float</span>(xa), <span class="bu">float</span>(xb)])</span>
<span id="cb7-63"><a href="#cb7-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> ist_zulaessig(p) <span class="kw">and</span> c <span class="op">@</span> p <span class="op">&gt;</span> bester_wert:</span>
<span id="cb7-64"><a href="#cb7-64" aria-hidden="true" tabindex="-1"></a> bester_wert, bester_punkt <span class="op">=</span> c <span class="op">@</span> p, p</span>
<span id="cb7-65"><a href="#cb7-65" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> bester_punkt, bester_wert</span>
<span id="cb7-66"><a href="#cb7-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-67"><a href="#cb7-67" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-68"><a href="#cb7-68" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Analyse ---------------------------------------------------------------</span></span>
<span id="cb7-69"><a href="#cb7-69" aria-hidden="true" tabindex="-1"></a>ecken <span class="op">=</span> berechne_ecken()</span>
<span id="cb7-70"><a href="#cb7-70" aria-hidden="true" tabindex="-1"></a>werte <span class="op">=</span> ecken <span class="op">@</span> c</span>
<span id="cb7-71"><a href="#cb7-71" aria-hidden="true" tabindex="-1"></a>reihenfolge <span class="op">=</span> np.argsort(<span class="op">-</span>werte)</span>
<span id="cb7-72"><a href="#cb7-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-73"><a href="#cb7-73" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">62</span>)</span>
<span id="cb7-74"><a href="#cb7-74" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot; ECKEN DES ZULÄSSIGEN POLYEDERS (nach Zielwert sortiert)&quot;</span>)</span>
<span id="cb7-75"><a href="#cb7-75" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">62</span>)</span>
<span id="cb7-76"><a href="#cb7-76" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;x_A&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;x_B&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Z = 150 xA + 250 xB&#39;</span><span class="sc">:&gt;24}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-77"><a href="#cb7-77" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">62</span>)</span>
<span id="cb7-78"><a href="#cb7-78" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> i <span class="kw">in</span> reihenfolge:</span>
<span id="cb7-79"><a href="#cb7-79" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>ecken[i, <span class="dv">0</span>]<span class="sc">:&gt;8.2f}</span><span class="ss"> </span><span class="sc">{</span>ecken[i, <span class="dv">1</span>]<span class="sc">:&gt;8.2f}</span><span class="ss"> </span><span class="sc">{</span>werte[i]<span class="sc">:&gt;24,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-80"><a href="#cb7-80" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-81"><a href="#cb7-81" aria-hidden="true" tabindex="-1"></a>lp_punkt, lp_wert <span class="op">=</span> ecken[reihenfolge[<span class="dv">0</span>]], werte[reihenfolge[<span class="dv">0</span>]]</span>
<span id="cb7-82"><a href="#cb7-82" aria-hidden="true" tabindex="-1"></a>ip_punkt, ip_wert <span class="op">=</span> bestes_ganzzahliges()</span>
<span id="cb7-83"><a href="#cb7-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-84"><a href="#cb7-84" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">62</span>)</span>
<span id="cb7-85"><a href="#cb7-85" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Kontinuierliches Optimum (LP): x = (</span><span class="sc">{</span>lp_punkt[<span class="dv">0</span>]<span class="sc">:.2f}</span><span class="ss">, </span><span class="sc">{</span>lp_punkt[<span class="dv">1</span>]<span class="sc">:.2f}</span><span class="ss">), &quot;</span></span>
<span id="cb7-86"><a href="#cb7-86" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Z = </span><span class="sc">{</span>lp_wert<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-87"><a href="#cb7-87" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Ganzzahliges Optimum (IP): x = (</span><span class="sc">{</span>ip_punkt[<span class="dv">0</span>]<span class="sc">:.0f}</span><span class="ss">, </span><span class="sc">{</span>ip_punkt[<span class="dv">1</span>]<span class="sc">:.0f}</span><span class="ss">), &quot;</span></span>
<span id="cb7-88"><a href="#cb7-88" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Z = </span><span class="sc">{</span>ip_wert<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-89"><a href="#cb7-89" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Preis der Ganzzahligkeit: </span><span class="sc">{</span>lp_wert <span class="op">-</span> ip_wert<span class="sc">:,.2f}</span><span class="ss"> EUR &quot;</span></span>
<span id="cb7-90"><a href="#cb7-90" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>(<span class="dv">1</span> <span class="op">-</span> ip_wert <span class="op">/</span> lp_wert) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb7-91"><a href="#cb7-91" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">62</span>)</span>
<span id="cb7-92"><a href="#cb7-92" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-93"><a href="#cb7-93" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Zeichnung -------------------------------------------------------------</span></span>
<span id="cb7-94"><a href="#cb7-94" aria-hidden="true" tabindex="-1"></a>gitter <span class="op">=</span> np.linspace(<span class="dv">0</span>, <span class="dv">15</span>, <span class="dv">400</span>)</span>
<span id="cb7-95"><a href="#cb7-95" aria-hidden="true" tabindex="-1"></a>xa_gitter, xb_gitter <span class="op">=</span> np.meshgrid(gitter, gitter)</span>
<span id="cb7-96"><a href="#cb7-96" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-97"><a href="#cb7-97" aria-hidden="true" tabindex="-1"></a>plt.figure(figsize<span class="op">=</span>(<span class="dv">10</span>, <span class="dv">8</span>))</span>
<span id="cb7-98"><a href="#cb7-98" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-99"><a href="#cb7-99" aria-hidden="true" tabindex="-1"></a><span class="co"># Restriktionsgeraden</span></span>
<span id="cb7-100"><a href="#cb7-100" aria-hidden="true" tabindex="-1"></a>plt.plot(gitter, (<span class="dv">40</span> <span class="op">-</span> <span class="dv">2</span> <span class="op">*</span> gitter) <span class="op">/</span> <span class="dv">5</span>, color<span class="op">=</span><span class="st">&quot;tab:blue&quot;</span>, lw<span class="op">=</span><span class="dv">2</span>,</span>
<span id="cb7-101"><a href="#cb7-101" aria-hidden="true" tabindex="-1"></a> label<span class="op">=</span><span class="vs">r&quot;$2x_A + 5x_B \leq 40$ (vCPU)&quot;</span>)</span>
<span id="cb7-102"><a href="#cb7-102" aria-hidden="true" tabindex="-1"></a>plt.plot(gitter, (<span class="dv">60</span> <span class="op">-</span> <span class="dv">4</span> <span class="op">*</span> gitter) <span class="op">/</span> <span class="dv">6</span>, color<span class="op">=</span><span class="st">&quot;tab:green&quot;</span>, lw<span class="op">=</span><span class="dv">2</span>,</span>
<span id="cb7-103"><a href="#cb7-103" aria-hidden="true" tabindex="-1"></a> label<span class="op">=</span><span class="vs">r&quot;$4x_A + 6x_B \leq 60$ (RAM)&quot;</span>)</span>
<span id="cb7-104"><a href="#cb7-104" aria-hidden="true" tabindex="-1"></a>plt.axvline(x<span class="op">=</span><span class="dv">8</span>, color<span class="op">=</span><span class="st">&quot;tab:orange&quot;</span>, lw<span class="op">=</span><span class="dv">2</span>, label<span class="op">=</span><span class="vs">r&quot;$x_A \leq 8$ (Marktlimit)&quot;</span>)</span>
<span id="cb7-105"><a href="#cb7-105" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-106"><a href="#cb7-106" aria-hidden="true" tabindex="-1"></a><span class="co"># Zulässiger Bereich</span></span>
<span id="cb7-107"><a href="#cb7-107" aria-hidden="true" tabindex="-1"></a>maske <span class="op">=</span> ((<span class="dv">2</span> <span class="op">*</span> xa_gitter <span class="op">+</span> <span class="dv">5</span> <span class="op">*</span> xb_gitter <span class="op">&lt;=</span> <span class="dv">40</span>) <span class="op">&amp;</span> (<span class="dv">4</span> <span class="op">*</span> xa_gitter <span class="op">+</span> <span class="dv">6</span> <span class="op">*</span> xb_gitter <span class="op">&lt;=</span> <span class="dv">60</span>)</span>
<span id="cb7-108"><a href="#cb7-108" aria-hidden="true" tabindex="-1"></a> <span class="op">&amp;</span> (xa_gitter <span class="op">&lt;=</span> <span class="dv">8</span>) <span class="op">&amp;</span> (xa_gitter <span class="op">&gt;=</span> <span class="dv">0</span>) <span class="op">&amp;</span> (xb_gitter <span class="op">&gt;=</span> <span class="dv">0</span>))</span>
<span id="cb7-109"><a href="#cb7-109" aria-hidden="true" tabindex="-1"></a>plt.imshow(maske.astype(<span class="bu">int</span>), extent<span class="op">=</span>(<span class="dv">0</span>, <span class="dv">15</span>, <span class="dv">0</span>, <span class="dv">15</span>), origin<span class="op">=</span><span class="st">&quot;lower&quot;</span>,</span>
<span id="cb7-110"><a href="#cb7-110" aria-hidden="true" tabindex="-1"></a> cmap<span class="op">=</span><span class="st">&quot;Greys&quot;</span>, alpha<span class="op">=</span><span class="fl">0.25</span>, aspect<span class="op">=</span><span class="st">&quot;auto&quot;</span>)</span>
<span id="cb7-111"><a href="#cb7-111" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-112"><a href="#cb7-112" aria-hidden="true" tabindex="-1"></a><span class="co"># Höhenlinien der Zielfunktion</span></span>
<span id="cb7-113"><a href="#cb7-113" aria-hidden="true" tabindex="-1"></a>Z <span class="op">=</span> <span class="dv">150</span> <span class="op">*</span> xa_gitter <span class="op">+</span> <span class="dv">250</span> <span class="op">*</span> xb_gitter</span>
<span id="cb7-114"><a href="#cb7-114" aria-hidden="true" tabindex="-1"></a>hoehen <span class="op">=</span> plt.contour(xa_gitter, xb_gitter, Z, levels<span class="op">=</span>[<span class="dv">500</span>, <span class="dv">1000</span>, <span class="dv">1500</span>, <span class="dv">2000</span>, <span class="dv">2375</span>],</span>
<span id="cb7-115"><a href="#cb7-115" aria-hidden="true" tabindex="-1"></a> colors<span class="op">=</span><span class="st">&quot;purple&quot;</span>, linestyles<span class="op">=</span><span class="st">&quot;--&quot;</span>, alpha<span class="op">=</span><span class="fl">0.7</span>)</span>
<span id="cb7-116"><a href="#cb7-116" aria-hidden="true" tabindex="-1"></a>plt.clabel(hoehen, inline<span class="op">=</span><span class="va">True</span>, fontsize<span class="op">=</span><span class="dv">9</span>, fmt<span class="op">=</span><span class="st">&quot;Z = %1.0f EUR&quot;</span>)</span>
<span id="cb7-117"><a href="#cb7-117" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-118"><a href="#cb7-118" aria-hidden="true" tabindex="-1"></a><span class="co"># Ecken einzeichnen - jetzt werden sie tatsächlich benutzt</span></span>
<span id="cb7-119"><a href="#cb7-119" aria-hidden="true" tabindex="-1"></a>plt.scatter(ecken[:, <span class="dv">0</span>], ecken[:, <span class="dv">1</span>], s<span class="op">=</span><span class="dv">70</span>, facecolors<span class="op">=</span><span class="st">&quot;white&quot;</span>,</span>
<span id="cb7-120"><a href="#cb7-120" aria-hidden="true" tabindex="-1"></a> edgecolors<span class="op">=</span><span class="st">&quot;black&quot;</span>, zorder<span class="op">=</span><span class="dv">4</span>, label<span class="op">=</span><span class="st">&quot;Ecken des Polyeders&quot;</span>)</span>
<span id="cb7-121"><a href="#cb7-121" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> e, w <span class="kw">in</span> <span class="bu">zip</span>(ecken, werte):</span>
<span id="cb7-122"><a href="#cb7-122" aria-hidden="true" tabindex="-1"></a> plt.annotate(<span class="ss">f&quot;(</span><span class="sc">{</span>e[<span class="dv">0</span>]<span class="sc">:.1f}</span><span class="ss">, </span><span class="sc">{</span>e[<span class="dv">1</span>]<span class="sc">:.1f}</span><span class="ss">)</span><span class="ch">\n</span><span class="ss">Z=</span><span class="sc">{</span>w<span class="sc">:,.0f}</span><span class="ss">&quot;</span>, (e[<span class="dv">0</span>], e[<span class="dv">1</span>]),</span>
<span id="cb7-123"><a href="#cb7-123" aria-hidden="true" tabindex="-1"></a> textcoords<span class="op">=</span><span class="st">&quot;offset points&quot;</span>, xytext<span class="op">=</span>(<span class="dv">6</span>, <span class="dv">6</span>), fontsize<span class="op">=</span><span class="dv">8</span>)</span>
<span id="cb7-124"><a href="#cb7-124" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-125"><a href="#cb7-125" aria-hidden="true" tabindex="-1"></a>plt.scatter([lp_punkt[<span class="dv">0</span>]], [lp_punkt[<span class="dv">1</span>]], color<span class="op">=</span><span class="st">&quot;purple&quot;</span>, marker<span class="op">=</span><span class="st">&quot;D&quot;</span>, s<span class="op">=</span><span class="dv">110</span>, zorder<span class="op">=</span><span class="dv">5</span>,</span>
<span id="cb7-126"><a href="#cb7-126" aria-hidden="true" tabindex="-1"></a> label<span class="op">=</span><span class="ss">f&quot;LP-Optimum (</span><span class="sc">{</span>lp_punkt[<span class="dv">0</span>]<span class="sc">:.1f}</span><span class="ss">, </span><span class="sc">{</span>lp_punkt[<span class="dv">1</span>]<span class="sc">:.1f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb7-127"><a href="#cb7-127" aria-hidden="true" tabindex="-1"></a>plt.scatter([ip_punkt[<span class="dv">0</span>]], [ip_punkt[<span class="dv">1</span>]], color<span class="op">=</span><span class="st">&quot;red&quot;</span>, s<span class="op">=</span><span class="dv">170</span>, zorder<span class="op">=</span><span class="dv">6</span>,</span>
<span id="cb7-128"><a href="#cb7-128" aria-hidden="true" tabindex="-1"></a> label<span class="op">=</span><span class="ss">f&quot;Ganzzahliges Optimum (</span><span class="sc">{</span>ip_punkt[<span class="dv">0</span>]<span class="sc">:.0f}</span><span class="ss">, </span><span class="sc">{</span>ip_punkt[<span class="dv">1</span>]<span class="sc">:.0f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb7-129"><a href="#cb7-129" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-130"><a href="#cb7-130" aria-hidden="true" tabindex="-1"></a>plt.xlim(<span class="dv">0</span>, <span class="dv">12</span>)</span>
<span id="cb7-131"><a href="#cb7-131" aria-hidden="true" tabindex="-1"></a>plt.ylim(<span class="dv">0</span>, <span class="dv">10</span>)</span>
<span id="cb7-132"><a href="#cb7-132" aria-hidden="true" tabindex="-1"></a>plt.xlabel(<span class="st">&quot;Anzahl Arbitrage-Bots ($x_A$)&quot;</span>, fontsize<span class="op">=</span><span class="dv">11</span>)</span>
<span id="cb7-133"><a href="#cb7-133" aria-hidden="true" tabindex="-1"></a>plt.ylabel(<span class="st">&quot;Anzahl Trendfolge-Bots ($x_B$)&quot;</span>, fontsize<span class="op">=</span><span class="dv">11</span>)</span>
<span id="cb7-134"><a href="#cb7-134" aria-hidden="true" tabindex="-1"></a>plt.title(<span class="st">&quot;Polyeder des zulässigen Bereichs mit Niveaulinien der Zielfunktion&quot;</span>, fontsize<span class="op">=</span><span class="dv">13</span>)</span>
<span id="cb7-135"><a href="#cb7-135" aria-hidden="true" tabindex="-1"></a>plt.grid(<span class="va">True</span>, linestyle<span class="op">=</span><span class="st">&quot;:&quot;</span>, alpha<span class="op">=</span><span class="fl">0.6</span>)</span>
<span id="cb7-136"><a href="#cb7-136" aria-hidden="true" tabindex="-1"></a>plt.legend(loc<span class="op">=</span><span class="st">&quot;upper right&quot;</span>, framealpha<span class="op">=</span><span class="fl">0.9</span>)</span>
<span id="cb7-137"><a href="#cb7-137" aria-hidden="true" tabindex="-1"></a>plt.tight_layout()</span>
<span id="cb7-138"><a href="#cb7-138" aria-hidden="true" tabindex="-1"></a>ziel <span class="op">=</span> os.path.join(OUTPUT_DIR, <span class="st">&quot;feasible_region_2d.png&quot;</span>)</span>
<span id="cb7-139"><a href="#cb7-139" aria-hidden="true" tabindex="-1"></a>plt.savefig(ziel, dpi<span class="op">=</span><span class="dv">150</span>)</span>
<span id="cb7-140"><a href="#cb7-140" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Visualisierung gespeichert unter &#39;</span><span class="sc">{</span>ziel<span class="sc">}</span><span class="ss">&#39;&quot;</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================
ECKEN DES ZULÄSSIGEN POLYEDERS (nach Zielwert sortiert)
==============================================================
x_A x_B Z = 150 xA + 250 xB
--------------------------------------------------------------
7.50 5.00 2,375.00 EUR
8.00 4.67 2,366.67 EUR
0.00 8.00 2,000.00 EUR
8.00 0.00 1,200.00 EUR
0.00 0.00 0.00 EUR
--------------------------------------------------------------
Kontinuierliches Optimum (LP): x = (7.50, 5.00), Z = 2,375.00 EUR
Ganzzahliges Optimum (IP): x = (7, 5), Z = 2,300.00 EUR
Preis der Ganzzahligkeit: 75.00 EUR (3.16 %)
==============================================================</code></pre>
<p>Zwei Beobachtungen, die <a href="milp.html#kap-milp">Kapitel 6</a> vorbereiten:</p>
<ol type="1">
<li>Das LP-Optimum liegt bei <span class="math inline">(7{,}5;\ 5{,}0)</span><strong>kein zulässiger Betriebszustand</strong>, denn ein halber Bot existiert nicht.</li>
<li>Das ganzzahlige Optimum <span class="math inline">(7, 5)</span> ist <strong>nicht</strong> durch Runden entstanden: Aufrunden auf <span class="math inline">(8, 5)</span> wäre unzulässig (<span class="math inline">2\cdot8+5\cdot5=41&gt;40</span>). Hier ging Abrunden gut — <a href="milp.html#kap-milp">Kapitel 6</a> zeigt Fälle, in denen Runden dramatisch scheitert.</li>
</ol>
<hr />
<h2 id="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>
</blockquote>
<figure>
<img src="bilder_04/kap_fundament_kondition.svg" alt="Abb. 2.3: 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." />
<figcaption aria-hidden="true">Abb. 2.3: Was <span class="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 — <span class="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>
</figure>
<p>Die Antwort trägt einen Namen: die <strong>Konditionszahl</strong> <span class="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>
<h3 id="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 <span class="math inline">\kappa(\mathbf{A})</span>: <strong>den Verstärkungsfaktor zwischen Datenfehler und Lösungsfehler.</strong></p>
<p><span class="math display">
\frac{\lVert \Delta \mathbf{x}\rVert}{\lVert \mathbf{x}\rVert}
\ \le\ \kappa(\mathbf{A}) \cdot \frac{\lVert \Delta \mathbf{b}\rVert}{\lVert \mathbf{b}\rVert},
\qquad
\kappa(\mathbf{A}) = \lVert \mathbf{A}\rVert \cdot \lVert \mathbf{A}^{-1}\rVert
= \frac{\sigma_{\max}}{\sigma_{\min}}
</span></p>
<div class="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer</strong></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Mathematik</th>
<th>Alltagssprache</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">\lVert \Delta\mathbf{b}\rVert / \lVert\mathbf{b}\rVert</span></td>
<td>„Um wie viel Prozent sind meine Eingabedaten daneben?“</td>
</tr>
<tr class="even">
<td><span class="math inline">\lVert \Delta\mathbf{x}\rVert / \lVert\mathbf{x}\rVert</span></td>
<td>„Um wie viel Prozent ist die Lösung daneben?“</td>
</tr>
<tr class="odd">
<td><span class="math inline">\kappa(\mathbf{A})</span></td>
<td>Der schlimmstmögliche Verstärkungsfaktor zwischen beidem.</td>
</tr>
<tr class="even">
<td><span class="math inline">\sigma_{\max}/\sigma_{\min}</span></td>
<td>Verhältnis der größten zur kleinsten Streckung, die die Matrix ausübt — „wie schief ist der Keil?“</td>
</tr>
<tr class="odd">
<td><span class="math inline">\kappa = 1</span></td>
<td>Bestfall: kein Fehler wird verstärkt.</td>
</tr>
<tr class="even">
<td><span class="math inline">\kappa = 10^{k}</span></td>
<td>Faustregel: Sie verlieren <span class="math inline">k</span> signifikante Stellen. Bei <span class="math inline">\kappa = 10^{8}</span> bleiben von 16 Stellen noch 8.</td>
</tr>
</tbody>
</table>
<p><strong>In einem Satz:</strong> <em><span class="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> <span class="math inline">\kappa &lt; 10^3</span> ist unbedenklich, <span class="math inline">\kappa &gt; 10^6</span> verdient eine Prüfung, <span class="math inline">\kappa &gt; 10^{10}</span> heißt: Das Ergebnis ist Rauschen mit Nachkommastellen. Rechnen Sie <span class="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>
<h3 id="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>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Ursache</th>
<th>Beispiel aus der Praxis</th>
<th>Abhilfe</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Gemischte Größenordnungen</td>
<td>Kapitalbindung in Euro (<span class="math inline">10^7</span>) neben Ausschussquote als Anteil (<span class="math inline">10^{-2}</span>) in derselben Matrix</td>
<td>In Tsd. Euro und Prozent rechnen — oder automatisch skalieren</td>
</tr>
<tr class="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>
</tr>
<tr class="odd">
<td>Big-M zu groß gewählt</td>
<td><span class="math inline">M = 10^9</span>, wo <span class="math inline">M = 500</span> genügt hätte (<a href="milp.html#kap-milp">Kapitel 6</a>)</td>
<td>Kleinstmögliches <span class="math inline">M</span> aus den Daten herleiten</td>
</tr>
<tr class="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 <span class="math inline">\kappa</span> auf und lässt Solver bei völlig harmlosen Modellen stundenlang suchen oder falsche Ergebnisse melden.</p>
<h3 id="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><span class="math display">
\tilde{\mathbf{A}} = \mathbf{D}_r\,\mathbf{A}\,\mathbf{D}_c
</span></p>
<p>mit Diagonalmatrizen <span class="math inline">\mathbf{D}_r</span> (Zeilen) und <span class="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 <span class="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>
<h3 id="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 <a href="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>
<div class="sourceCode" id="cb9"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Skalierung_Kondition.py</span></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Fundament: Was die Konditionszahl kappa(A) praktisch bedeutet - und wie man</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a><span class="co">ein schlecht skaliertes Modell wieder gesund rechnet.</span></span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a><span class="co">Drei Experimente:</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a><span class="co"> 1. Fehlerverstaerkung: Wie stark schlaegt eine winzige Datenunsicherheit auf</span></span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a><span class="co"> die Loesung durch? kappa(A) ist genau die Obergrenze dieses Faktors.</span></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. Ruiz-Equilibrierung: Ein Modell, in dem Euro-Betraege (1e7) und</span></span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a><span class="co"> Tonnen-Angaben (1e-3) in derselben Matrix stehen, wird durch Zeilen- und</span></span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a><span class="co"> Spaltenskalierung um Groessenordnungen besser konditioniert.</span></span>
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a><span class="co"> 3. Toleranzen: Warum eine Binaervariable mit dem Wert 0.99999998 niemals</span></span>
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a><span class="co"> mit int() gerundet werden darf.</span></span>
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, scipy</span></span>
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">42</span>)</span>
<span id="cb9-27"><a href="#cb9-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-28"><a href="#cb9-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Experiment 1: kappa(A) als Fehlerverstaerker ---------------------------</span></span>
<span id="cb9-30"><a href="#cb9-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-31"><a href="#cb9-31" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> fehlerverstaerkung(A: np.ndarray, versuche: <span class="bu">int</span> <span class="op">=</span> <span class="dv">200</span>,</span>
<span id="cb9-32"><a href="#cb9-32" aria-hidden="true" tabindex="-1"></a> stoerung: <span class="bu">float</span> <span class="op">=</span> <span class="fl">1e-10</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">float</span>, <span class="bu">float</span>]:</span>
<span id="cb9-33"><a href="#cb9-33" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Stoert die rechte Seite b relativ um &#39;stoerung&#39; in zufaellige Richtungen</span></span>
<span id="cb9-34"><a href="#cb9-34" aria-hidden="true" tabindex="-1"></a><span class="co"> und misst, um welchen Faktor sich der Fehler in der Loesung x vergroessert.</span></span>
<span id="cb9-35"><a href="#cb9-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-36"><a href="#cb9-36" aria-hidden="true" tabindex="-1"></a><span class="co"> Liefert (Median, Maximum) der Verstaerkung. Die Theorie sagt: Das Maximum</span></span>
<span id="cb9-37"><a href="#cb9-37" aria-hidden="true" tabindex="-1"></a><span class="co"> kann bis kappa(A) betragen - und nur bis dahin.</span></span>
<span id="cb9-38"><a href="#cb9-38" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb9-39"><a href="#cb9-39" aria-hidden="true" tabindex="-1"></a> x_wahr <span class="op">=</span> np.ones(A.shape[<span class="dv">1</span>])</span>
<span id="cb9-40"><a href="#cb9-40" aria-hidden="true" tabindex="-1"></a> b <span class="op">=</span> A <span class="op">@</span> x_wahr</span>
<span id="cb9-41"><a href="#cb9-41" aria-hidden="true" tabindex="-1"></a> faktoren <span class="op">=</span> []</span>
<span id="cb9-42"><a href="#cb9-42" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(versuche):</span>
<span id="cb9-43"><a href="#cb9-43" aria-hidden="true" tabindex="-1"></a> richtung <span class="op">=</span> RNG.normal(size<span class="op">=</span>b.size)</span>
<span id="cb9-44"><a href="#cb9-44" aria-hidden="true" tabindex="-1"></a> richtung <span class="op">/=</span> np.linalg.norm(richtung)</span>
<span id="cb9-45"><a href="#cb9-45" aria-hidden="true" tabindex="-1"></a> b_gestoert <span class="op">=</span> b <span class="op">+</span> stoerung <span class="op">*</span> np.linalg.norm(b) <span class="op">*</span> richtung</span>
<span id="cb9-46"><a href="#cb9-46" aria-hidden="true" tabindex="-1"></a> x_gestoert <span class="op">=</span> np.linalg.solve(A, b_gestoert)</span>
<span id="cb9-47"><a href="#cb9-47" aria-hidden="true" tabindex="-1"></a> rel_x <span class="op">=</span> np.linalg.norm(x_gestoert <span class="op">-</span> x_wahr) <span class="op">/</span> np.linalg.norm(x_wahr)</span>
<span id="cb9-48"><a href="#cb9-48" aria-hidden="true" tabindex="-1"></a> faktoren.append(rel_x <span class="op">/</span> stoerung)</span>
<span id="cb9-49"><a href="#cb9-49" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">float</span>(np.median(faktoren)), <span class="bu">float</span>(np.<span class="bu">max</span>(faktoren))</span>
<span id="cb9-50"><a href="#cb9-50" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-51"><a href="#cb9-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-52"><a href="#cb9-52" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige_experiment_1() <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb9-53"><a href="#cb9-53" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-54"><a href="#cb9-54" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 1. KONDITIONSZAHL ALS FEHLERVERSTAERKER&quot;</span>)</span>
<span id="cb9-55"><a href="#cb9-55" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-56"><a href="#cb9-56" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Zwei Gleichungssysteme, beide exakt loesbar mit x = (1, 1).&quot;</span>)</span>
<span id="cb9-57"><a href="#cb9-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Die rechte Seite wird um relativ 1e-10 gestoert - so viel Unsicherheit&quot;</span>)</span>
<span id="cb9-58"><a href="#cb9-58" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;steckt in JEDER gemessenen Betriebszahl allemal.</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb9-59"><a href="#cb9-59" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-60"><a href="#cb9-60" aria-hidden="true" tabindex="-1"></a> modelle <span class="op">=</span> {</span>
<span id="cb9-61"><a href="#cb9-61" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;gut konditioniert&quot;</span>: np.array([[<span class="fl">2.0</span>, <span class="fl">1.0</span>], [<span class="fl">1.0</span>, <span class="fl">3.0</span>]]),</span>
<span id="cb9-62"><a href="#cb9-62" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;fast parallele Zeilen&quot;</span>: np.array([[<span class="fl">1.0</span>, <span class="fl">1.0</span>], [<span class="fl">1.0</span>, <span class="fl">1.0</span> <span class="op">+</span> <span class="fl">1e-8</span>]]),</span>
<span id="cb9-63"><a href="#cb9-63" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb9-64"><a href="#cb9-64" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Matrix&#39;</span><span class="sc">:&lt;24}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;kappa(A)&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Verst. median&#39;</span><span class="sc">:&gt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Verst. max&#39;</span><span class="sc">:&gt;12}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-65"><a href="#cb9-65" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-66"><a href="#cb9-66" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, A <span class="kw">in</span> modelle.items():</span>
<span id="cb9-67"><a href="#cb9-67" aria-hidden="true" tabindex="-1"></a> median, maximum <span class="op">=</span> fehlerverstaerkung(A)</span>
<span id="cb9-68"><a href="#cb9-68" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;24}</span><span class="ss"> </span><span class="sc">{</span>np<span class="sc">.</span>linalg<span class="sc">.</span>cond(A)<span class="sc">:&gt;12.2e}</span><span class="ss"> </span><span class="sc">{</span>median<span class="sc">:&gt;14.2e}</span><span class="ss"> </span><span class="sc">{</span>maximum<span class="sc">:&gt;12.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-69"><a href="#cb9-69" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-70"><a href="#cb9-70" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Lesart: Bei der zweiten Matrix wird aus einem Datenfehler in der&quot;</span>)</span>
<span id="cb9-71"><a href="#cb9-71" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;10. Nachkommastelle ein Loesungsfehler in der 2. Nachkommastelle.&quot;</span>)</span>
<span id="cb9-72"><a href="#cb9-72" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Das Modell ist mathematisch korrekt - und praktisch wertlos.&quot;</span>)</span>
<span id="cb9-73"><a href="#cb9-73" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-74"><a href="#cb9-74" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-75"><a href="#cb9-75" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Experiment 2: Ruiz-Equilibrierung --------------------------------------</span></span>
<span id="cb9-76"><a href="#cb9-76" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-77"><a href="#cb9-77" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> ruiz_equilibrierung(A: np.ndarray, durchlaeufe: <span class="bu">int</span> <span class="op">=</span> <span class="dv">20</span></span>
<span id="cb9-78"><a href="#cb9-78" aria-hidden="true" tabindex="-1"></a> ) <span class="op">-&gt;</span> <span class="bu">tuple</span>[np.ndarray, np.ndarray, np.ndarray]:</span>
<span id="cb9-79"><a href="#cb9-79" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Skaliert A iterativ so, dass alle Zeilen- und Spaltenmaxima nahe 1</span></span>
<span id="cb9-80"><a href="#cb9-80" aria-hidden="true" tabindex="-1"></a><span class="co"> liegen (Ruiz 2001).</span></span>
<span id="cb9-81"><a href="#cb9-81" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-82"><a href="#cb9-82" aria-hidden="true" tabindex="-1"></a><span class="co"> In jedem Durchlauf wird jede Zeile durch die Wurzel ihres Betragsmaximums</span></span>
<span id="cb9-83"><a href="#cb9-83" aria-hidden="true" tabindex="-1"></a><span class="co"> geteilt, danach jede Spalte. Das Verfahren konvergiert schnell und braucht</span></span>
<span id="cb9-84"><a href="#cb9-84" aria-hidden="true" tabindex="-1"></a><span class="co"> keinerlei Wissen ueber die Bedeutung der Zahlen - genau deshalb steckt es</span></span>
<span id="cb9-85"><a href="#cb9-85" aria-hidden="true" tabindex="-1"></a><span class="co"> in jedem ernsthaften Solver als Vorverarbeitung.</span></span>
<span id="cb9-86"><a href="#cb9-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-87"><a href="#cb9-87" aria-hidden="true" tabindex="-1"></a><span class="co"> Liefert (A_skaliert, zeilenfaktor r, spaltenfaktor c) mit</span></span>
<span id="cb9-88"><a href="#cb9-88" aria-hidden="true" tabindex="-1"></a><span class="co"> A_skaliert = diag(r) @ A @ diag(c)</span></span>
<span id="cb9-89"><a href="#cb9-89" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb9-90"><a href="#cb9-90" aria-hidden="true" tabindex="-1"></a> m, n <span class="op">=</span> A.shape</span>
<span id="cb9-91"><a href="#cb9-91" aria-hidden="true" tabindex="-1"></a> r <span class="op">=</span> np.ones(m)</span>
<span id="cb9-92"><a href="#cb9-92" aria-hidden="true" tabindex="-1"></a> c <span class="op">=</span> np.ones(n)</span>
<span id="cb9-93"><a href="#cb9-93" aria-hidden="true" tabindex="-1"></a> A_s <span class="op">=</span> A.astype(<span class="bu">float</span>).copy()</span>
<span id="cb9-94"><a href="#cb9-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-95"><a href="#cb9-95" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(durchlaeufe):</span>
<span id="cb9-96"><a href="#cb9-96" aria-hidden="true" tabindex="-1"></a> zeilen_max <span class="op">=</span> np.<span class="bu">abs</span>(A_s).<span class="bu">max</span>(axis<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb9-97"><a href="#cb9-97" aria-hidden="true" tabindex="-1"></a> zeilen_max[zeilen_max <span class="op">==</span> <span class="dv">0</span>] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb9-98"><a href="#cb9-98" aria-hidden="true" tabindex="-1"></a> d_r <span class="op">=</span> <span class="fl">1.0</span> <span class="op">/</span> np.sqrt(zeilen_max)</span>
<span id="cb9-99"><a href="#cb9-99" aria-hidden="true" tabindex="-1"></a> A_s <span class="op">=</span> d_r[:, <span class="va">None</span>] <span class="op">*</span> A_s</span>
<span id="cb9-100"><a href="#cb9-100" aria-hidden="true" tabindex="-1"></a> r <span class="op">*=</span> d_r</span>
<span id="cb9-101"><a href="#cb9-101" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-102"><a href="#cb9-102" aria-hidden="true" tabindex="-1"></a> spalten_max <span class="op">=</span> np.<span class="bu">abs</span>(A_s).<span class="bu">max</span>(axis<span class="op">=</span><span class="dv">0</span>)</span>
<span id="cb9-103"><a href="#cb9-103" aria-hidden="true" tabindex="-1"></a> spalten_max[spalten_max <span class="op">==</span> <span class="dv">0</span>] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb9-104"><a href="#cb9-104" aria-hidden="true" tabindex="-1"></a> d_c <span class="op">=</span> <span class="fl">1.0</span> <span class="op">/</span> np.sqrt(spalten_max)</span>
<span id="cb9-105"><a href="#cb9-105" aria-hidden="true" tabindex="-1"></a> A_s <span class="op">=</span> A_s <span class="op">*</span> d_c[<span class="va">None</span>, :]</span>
<span id="cb9-106"><a href="#cb9-106" aria-hidden="true" tabindex="-1"></a> c <span class="op">*=</span> d_c</span>
<span id="cb9-107"><a href="#cb9-107" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-108"><a href="#cb9-108" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> A_s, r, c</span>
<span id="cb9-109"><a href="#cb9-109" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-110"><a href="#cb9-110" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-111"><a href="#cb9-111" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige_experiment_2() <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb9-112"><a href="#cb9-112" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-113"><a href="#cb9-113" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 2. RUIZ-EQUILIBRIERUNG: EINHEITEN GERADERUECKEN&quot;</span>)</span>
<span id="cb9-114"><a href="#cb9-114" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-115"><a href="#cb9-115" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Ein Produktionsmodell, in dem vier Ressourcen in voellig&quot;</span>)</span>
<span id="cb9-116"><a href="#cb9-116" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;verschiedenen Einheiten gemessen werden:&quot;</span>)</span>
<span id="cb9-117"><a href="#cb9-117" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Zeile 1: Kapitalbindung in Euro (Groessenordnung 1e7)&quot;</span>)</span>
<span id="cb9-118"><a href="#cb9-118" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Zeile 2: Katalysatorverbrauch in Tonnen (Groessenordnung 1e-3)&quot;</span>)</span>
<span id="cb9-119"><a href="#cb9-119" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Zeile 3: Energie in Wattsekunden (Groessenordnung 1e5)&quot;</span>)</span>
<span id="cb9-120"><a href="#cb9-120" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Zeile 4: Ausschussquote als Anteil (Groessenordnung 1e-2)</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb9-121"><a href="#cb9-121" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-122"><a href="#cb9-122" aria-hidden="true" tabindex="-1"></a> <span class="co"># Quadratisch gewaehlt, damit die Loesung eindeutig ist und die</span></span>
<span id="cb9-123"><a href="#cb9-123" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ruecktransformation unten wirklich etwas beweist.</span></span>
<span id="cb9-124"><a href="#cb9-124" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="dv">4</span></span>
<span id="cb9-125"><a href="#cb9-125" aria-hidden="true" tabindex="-1"></a> grundmatrix <span class="op">=</span> RNG.uniform(<span class="fl">0.5</span>, <span class="fl">2.0</span>, size<span class="op">=</span>(n, n))</span>
<span id="cb9-126"><a href="#cb9-126" aria-hidden="true" tabindex="-1"></a> einheiten <span class="op">=</span> np.array([<span class="fl">1e7</span>, <span class="fl">1e-3</span>, <span class="fl">1e5</span>, <span class="fl">1e-2</span>])</span>
<span id="cb9-127"><a href="#cb9-127" aria-hidden="true" tabindex="-1"></a> A <span class="op">=</span> grundmatrix <span class="op">*</span> einheiten[:, <span class="va">None</span>]</span>
<span id="cb9-128"><a href="#cb9-128" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-129"><a href="#cb9-129" aria-hidden="true" tabindex="-1"></a> A_s, r, c <span class="op">=</span> ruiz_equilibrierung(A)</span>
<span id="cb9-130"><a href="#cb9-130" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-131"><a href="#cb9-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;28}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;kappa(A)&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;groesster Eintrag&#39;</span><span class="sc">:&gt;18}</span><span class="ss"> &quot;</span></span>
<span id="cb9-132"><a href="#cb9-132" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;kleinster Eintrag&#39;</span><span class="sc">:&gt;18}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-133"><a href="#cb9-133" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-134"><a href="#cb9-134" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, matrix <span class="kw">in</span> [(<span class="st">&quot;vor der Skalierung&quot;</span>, A), (<span class="st">&quot;nach Ruiz-Equilibrierung&quot;</span>, A_s)]:</span>
<span id="cb9-135"><a href="#cb9-135" aria-hidden="true" tabindex="-1"></a> betraege <span class="op">=</span> np.<span class="bu">abs</span>(matrix)</span>
<span id="cb9-136"><a href="#cb9-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;28}</span><span class="ss"> </span><span class="sc">{</span>np<span class="sc">.</span>linalg<span class="sc">.</span>cond(matrix)<span class="sc">:&gt;12.2e}</span><span class="ss"> &quot;</span></span>
<span id="cb9-137"><a href="#cb9-137" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>betraege<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:&gt;18.2e}</span><span class="ss"> </span><span class="sc">{</span>betraege<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:&gt;18.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-138"><a href="#cb9-138" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-139"><a href="#cb9-139" aria-hidden="true" tabindex="-1"></a> <span class="co"># Gegenprobe: Das skalierte Modell beschreibt dasselbe Problem. Wer x_s</span></span>
<span id="cb9-140"><a href="#cb9-140" aria-hidden="true" tabindex="-1"></a> <span class="co"># loest, erhaelt die urspruengliche Loesung durch x = c * x_s.</span></span>
<span id="cb9-141"><a href="#cb9-141" aria-hidden="true" tabindex="-1"></a> x_wahr <span class="op">=</span> RNG.uniform(<span class="fl">1.0</span>, <span class="fl">5.0</span>, size<span class="op">=</span>n)</span>
<span id="cb9-142"><a href="#cb9-142" aria-hidden="true" tabindex="-1"></a> b <span class="op">=</span> A <span class="op">@</span> x_wahr</span>
<span id="cb9-143"><a href="#cb9-143" aria-hidden="true" tabindex="-1"></a> b_s <span class="op">=</span> r <span class="op">*</span> b</span>
<span id="cb9-144"><a href="#cb9-144" aria-hidden="true" tabindex="-1"></a> x_s <span class="op">=</span> np.linalg.solve(A_s, b_s)</span>
<span id="cb9-145"><a href="#cb9-145" aria-hidden="true" tabindex="-1"></a> x_zurueck <span class="op">=</span> c <span class="op">*</span> x_s</span>
<span id="cb9-146"><a href="#cb9-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Ruecktransformation x = c * x_s: groesste Abweichung zur wahren &quot;</span></span>
<span id="cb9-147"><a href="#cb9-147" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Loesung </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">abs</span>(x_zurueck <span class="op">-</span> x_wahr)<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-148"><a href="#cb9-148" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Die Skalierung ist also verlustfrei - sie aendert nur die Zahlen,&quot;</span>)</span>
<span id="cb9-149"><a href="#cb9-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;nicht das Problem.&quot;</span>)</span>
<span id="cb9-150"><a href="#cb9-150" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-151"><a href="#cb9-151" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-152"><a href="#cb9-152" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Experiment 3: Toleranzen und der int()-Fehler --------------------------</span></span>
<span id="cb9-153"><a href="#cb9-153" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-154"><a href="#cb9-154" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige_experiment_3() <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb9-155"><a href="#cb9-155" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-156"><a href="#cb9-156" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 3. TOLERANZEN: WARUM int() DIE FALSCHE RUNDUNG IST&quot;</span>)</span>
<span id="cb9-157"><a href="#cb9-157" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-158"><a href="#cb9-158" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-159"><a href="#cb9-159" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ein LP, dessen Optimum bei x = 1 liegt, aber vom Solver nur bis auf</span></span>
<span id="cb9-160"><a href="#cb9-160" aria-hidden="true" tabindex="-1"></a> <span class="co"># seine Toleranz getroffen wird.</span></span>
<span id="cb9-161"><a href="#cb9-161" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=</span>[<span class="op">-</span><span class="fl">1.0</span>], A_ub<span class="op">=</span>[[<span class="fl">1.0</span>]], b_ub<span class="op">=</span>[<span class="fl">1.0</span>],</span>
<span id="cb9-162"><a href="#cb9-162" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)], method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb9-163"><a href="#cb9-163" aria-hidden="true" tabindex="-1"></a> wert <span class="op">=</span> <span class="bu">float</span>(ergebnis.x[<span class="dv">0</span>])</span>
<span id="cb9-164"><a href="#cb9-164" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Solver liefert x = </span><span class="sc">{</span>wert<span class="sc">!r}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-165"><a href="#cb9-165" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-166"><a href="#cb9-166" aria-hidden="true" tabindex="-1"></a> <span class="co"># Typische Werte, wie sie aus MILP-Solvern zurueckkommen.</span></span>
<span id="cb9-167"><a href="#cb9-167" aria-hidden="true" tabindex="-1"></a> beispiele <span class="op">=</span> [<span class="fl">0.99999998</span>, <span class="fl">1.00000002</span>, <span class="fl">0.49999999</span>, <span class="fl">2.9999999</span>]</span>
<span id="cb9-168"><a href="#cb9-168" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span><span class="st">&#39;Solverwert&#39;</span><span class="sc">:&gt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;int()&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;round()&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Kommentar&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-169"><a href="#cb9-169" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-170"><a href="#cb9-170" aria-hidden="true" tabindex="-1"></a> kommentare <span class="op">=</span> {</span>
<span id="cb9-171"><a href="#cb9-171" aria-hidden="true" tabindex="-1"></a> <span class="fl">0.99999998</span>: <span class="st">&quot;int() macht aus einer JA- eine NEIN-Entscheidung&quot;</span>,</span>
<span id="cb9-172"><a href="#cb9-172" aria-hidden="true" tabindex="-1"></a> <span class="fl">1.00000002</span>: <span class="st">&quot;hier ginge int() zufaellig gut - Verlass ist keiner&quot;</span>,</span>
<span id="cb9-173"><a href="#cb9-173" aria-hidden="true" tabindex="-1"></a> <span class="fl">0.49999999</span>: <span class="st">&quot;echt unentschieden: Modell oder Toleranz pruefen!&quot;</span>,</span>
<span id="cb9-174"><a href="#cb9-174" aria-hidden="true" tabindex="-1"></a> <span class="fl">2.9999999</span>: <span class="st">&quot;3 Maschinen werden zu 2 - der Plan geht nicht auf&quot;</span>,</span>
<span id="cb9-175"><a href="#cb9-175" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb9-176"><a href="#cb9-176" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> wert_b <span class="kw">in</span> beispiele:</span>
<span id="cb9-177"><a href="#cb9-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>wert_b<span class="sc">:&gt;14.8f}</span><span class="ss"> </span><span class="sc">{</span><span class="bu">int</span>(wert_b)<span class="sc">:&gt;8}</span><span class="ss"> </span><span class="sc">{</span><span class="bu">round</span>(wert_b)<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb9-178"><a href="#cb9-178" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>kommentare[wert_b]<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-179"><a href="#cb9-179" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-180"><a href="#cb9-180" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Richtige Vorgehensweise: gegen die Solver-Toleranz pruefen,&quot;</span>)</span>
<span id="cb9-181"><a href="#cb9-181" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;dann erst runden - und den Zweifelsfall melden statt still zu raten.&quot;</span>)</span>
<span id="cb9-182"><a href="#cb9-182" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-183"><a href="#cb9-183" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> sichere_ganzzahl(wert: <span class="bu">float</span>, toleranz: <span class="bu">float</span> <span class="op">=</span> <span class="fl">1e-6</span>) <span class="op">-&gt;</span> <span class="bu">int</span>:</span>
<span id="cb9-184"><a href="#cb9-184" aria-hidden="true" tabindex="-1"></a> naechste <span class="op">=</span> <span class="bu">round</span>(wert)</span>
<span id="cb9-185"><a href="#cb9-185" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(wert <span class="op">-</span> naechste) <span class="op">&gt;</span> toleranz:</span>
<span id="cb9-186"><a href="#cb9-186" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(</span>
<span id="cb9-187"><a href="#cb9-187" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>wert<span class="sc">}</span><span class="ss"> ist </span><span class="sc">{</span><span class="bu">abs</span>(wert <span class="op">-</span> naechste)<span class="sc">:.2e}</span><span class="ss"> von der naechsten ganzen &quot;</span></span>
<span id="cb9-188"><a href="#cb9-188" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Zahl entfernt - das ist mehr als die Toleranz </span><span class="sc">{</span>toleranz<span class="sc">}</span><span class="ss">. &quot;</span></span>
<span id="cb9-189"><a href="#cb9-189" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Ganzzahligkeit im Modell pruefen.&quot;</span>)</span>
<span id="cb9-190"><a href="#cb9-190" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> naechste</span>
<span id="cb9-191"><a href="#cb9-191" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-192"><a href="#cb9-192" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> wert_b <span class="kw">in</span> beispiele:</span>
<span id="cb9-193"><a href="#cb9-193" aria-hidden="true" tabindex="-1"></a> <span class="cf">try</span>:</span>
<span id="cb9-194"><a href="#cb9-194" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; sichere_ganzzahl(</span><span class="sc">{</span>wert_b<span class="sc">}</span><span class="ss">) = </span><span class="sc">{</span>sichere_ganzzahl(wert_b)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-195"><a href="#cb9-195" aria-hidden="true" tabindex="-1"></a> <span class="cf">except</span> <span class="pp">ValueError</span> <span class="im">as</span> fehler:</span>
<span id="cb9-196"><a href="#cb9-196" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; sichere_ganzzahl(</span><span class="sc">{</span>wert_b<span class="sc">}</span><span class="ss">) -&gt; ValueError: </span><span class="sc">{</span>fehler<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-197"><a href="#cb9-197" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-198"><a href="#cb9-198" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-199"><a href="#cb9-199" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb9-200"><a href="#cb9-200" aria-hidden="true" tabindex="-1"></a> zeige_experiment_1()</span>
<span id="cb9-201"><a href="#cb9-201" aria-hidden="true" tabindex="-1"></a> zeige_experiment_2()</span>
<span id="cb9-202"><a href="#cb9-202" aria-hidden="true" tabindex="-1"></a> zeige_experiment_3()</span>
<span id="cb9-203"><a href="#cb9-203" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb9-204"><a href="#cb9-204" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Merksatz: Skalieren Sie Ihre Daten, BEVOR der Solver sie sieht -&quot;</span>)</span>
<span id="cb9-205"><a href="#cb9-205" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;und runden Sie Solver-Ergebnisse NIE ohne Toleranzpruefung.&quot;</span>)</span>
<span id="cb9-206"><a href="#cb9-206" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==========================================================================
1. KONDITIONSZAHL ALS FEHLERVERSTAERKER
==========================================================================
Zwei Gleichungssysteme, beide exakt loesbar mit x = (1, 1).
Die rechte Seite wird um relativ 1e-10 gestoert - so viel Unsicherheit
steckt in JEDER gemessenen Betriebszahl allemal.
Matrix kappa(A) Verst. median Verst. max
--------------------------------------------------------------------------
gut konditioniert 2.62e+00 1.86e+00 2.56e+00
fast parallele Zeilen 4.00e+08 2.57e+08 4.00e+08
Lesart: Bei der zweiten Matrix wird aus einem Datenfehler in der
10. Nachkommastelle ein Loesungsfehler in der 2. Nachkommastelle.
Das Modell ist mathematisch korrekt - und praktisch wertlos.
==========================================================================
2. RUIZ-EQUILIBRIERUNG: EINHEITEN GERADERUECKEN
==========================================================================
Ein Produktionsmodell, in dem vier Ressourcen in voellig
verschiedenen Einheiten gemessen werden:
Zeile 1: Kapitalbindung in Euro (Groessenordnung 1e7)
Zeile 2: Katalysatorverbrauch in Tonnen (Groessenordnung 1e-3)
Zeile 3: Energie in Wattsekunden (Groessenordnung 1e5)
Zeile 4: Ausschussquote als Anteil (Groessenordnung 1e-2)
kappa(A) groesster Eintrag kleinster Eintrag
--------------------------------------------------------------------------
vor der Skalierung 8.67e+10 1.52e+07 1.35e-03
nach Ruiz-Equilibrierung 4.69e+01 1.00e+00 4.24e-01
Ruecktransformation x = c * x_s: groesste Abweichung zur wahren Loesung 2.35e-14
Die Skalierung ist also verlustfrei - sie aendert nur die Zahlen,
nicht das Problem.
==========================================================================
3. TOLERANZEN: WARUM int() DIE FALSCHE RUNDUNG IST
==========================================================================
Solver liefert x = 1.0
Solverwert int() round() Kommentar
--------------------------------------------------------------------------
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) -&gt; 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.
sichere_ganzzahl(2.9999999) = 3
==========================================================================
Merksatz: Skalieren Sie Ihre Daten, BEVOR der Solver sie sieht -
und runden Sie Solver-Ergebnisse NIE ohne Toleranzpruefung.
==========================================================================</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
<th>Warum das die Kernaussage trägt</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Störung in <strong>zufälliger Richtung</strong></td>
<td>200 Versuche statt einer festen Störung</td>
<td>Eine proportionale Störung (<span class="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>
</tr>
<tr class="even">
<td><code>Verst. max = 4.00e+08</code></td>
<td>erreicht exakt <span class="math inline">\kappa(\mathbf{A})</span></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>
</tr>
<tr class="odd">
<td>Ruiz: quadratische Matrix</td>
<td><span class="math inline">4 \times 4</span> statt <span class="math inline">4 \times 6</span></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>
<tr class="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>
<tr class="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 <span class="math inline">10^9</span> „damit es garantiert reicht“ verschlechtert <span class="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><span class="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>
</ul>
</blockquote>
<hr />
<h2 id="sec:fundament-uebungsaufgaben">2.8 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-fundament">Abschnitt A.2</a>.</p>
</blockquote>
<p><strong>Aufgabe 2.1 ⭐ — Matrixform lesen.</strong> Gegeben <span class="math inline">\mathbf{c} = (4, 1, 6)^\top</span>, <span class="math inline">\mathbf{A} = \begin{pmatrix} 1 &amp; 2 &amp; 0 \\ 0 &amp; 1 &amp; 3 \end{pmatrix}</span>, <span class="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 <span class="math inline">\min \mathbf{c}^\top\mathbf{x}</span> u. d. N. <span class="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span>: <span class="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 <span class="math inline">\max 2x_1 + 3x_2</span> u. d. N. <span class="math inline">x_1 + x_2 \le 8</span>, <span class="math inline">2x_1 + x_2 \le 12</span>, <span class="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 <span class="math inline">Z</span> in jeder Ecke aus und geben Sie das Optimum an. (d) Was passiert, wenn die Zielfunktion zu <span class="math inline">\max 2x_1 + 2x_2</span> wird? Wie viele optimale Lösungen gibt es dann?</p>
<p><strong>Aufgabe 2.4 ⭐⭐ — Konvexität beurteilen.</strong> Konvex oder nicht? Begründen Sie jeweils kurz: (a) <span class="math inline">f(x) = 5x + 3</span> (b) <span class="math inline">f(x) = x^4</span> (c) <span class="math inline">f(x) = \sqrt{x}</span> für <span class="math inline">x \ge 0</span> (d) <span class="math inline">f(x) = \ln(x)</span> für <span class="math inline">x &gt; 0</span> (e) <span class="math inline">f(x_1,x_2) = x_1^2 + x_2^2</span> (f) Die Menge <span class="math inline">\{(x_1,x_2) : x_1^2 + x_2^2 \le 4\}</span> (g) Die Menge <span class="math inline">\{(x_1,x_2) : x_1 \cdot x_2 \ge 1,\ x_1,x_2 &gt; 0\}</span></p>
<p><strong>Aufgabe 2.5 ⭐⭐ — Positive Semidefinitheit prüfen.</strong> Prüfen Sie mit NumPy, welche dieser Matrizen als Kovarianzmatrix taugen (alle Eigenwerte <span class="math inline">\ge 0</span>), und geben Sie bei Nichteignung die implizierte Korrelation an: <span class="math display">\mathbf{P}_1=\begin{pmatrix}4&amp;1\\1&amp;9\end{pmatrix},\quad \mathbf{P}_2=\begin{pmatrix}4&amp;7\\7&amp;9\end{pmatrix},\quad \mathbf{P}_3=\begin{pmatrix}1&amp;0.5&amp;0.5\\0.5&amp;1&amp;0.5\\0.5&amp;0.5&amp;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 <a href="einfuehrung.html#kap-einfuehrung">Kapitel 1</a> (die Bäckerei-Handrechnung aus <a href="einfuehrung.html#kap-einfuehrung">Kapitel 1</a>) an: <span class="math inline">\max 2{,}5x_1 + 3{,}0x_2</span> u. d. N. <span class="math inline">0{,}5x_1+0{,}6x_2 \le 90</span>, <span class="math inline">4x_1+3x_2 \le 600</span>, <span class="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 <span class="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>): <span class="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 />
<h2 id="sec:fundament-denkfehler">2.9 Finde den Denkfehler</h2>
<div class="card card-denkfehler">
<blockquote>
Sechster und siebter Fund: die letzten handgeschriebenen Nummern Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**", saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt. Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist keine Tabellenzelle. Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n - nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile schon taten. nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus {#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23. Im Quelltext steht "**{loesung} — Titel.**". Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie "siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler} umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12 den Kapitelnamen statt eines Verweises. Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer. Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit dem Titel des Raetsels ankuendigen. --check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M — im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele {loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau null. Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118 Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12 Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder inhaltlich veraenderte Zeile. Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327 Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit des Anbieters, nicht an dieser Aenderung). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:52:41 +02:00
<p><strong>🐛 Finde den Denkfehler: Die unauffällige Transposition</strong></p>
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<p>Ein Kollege überträgt ein Produktionsmodell in Matrixform. Zwei Produkte, zwei Ressourcen — die Technologiematrix ist quadratisch:</p>
<p><span class="math display">\max\ 3x_1 + 5x_2 \quad \text{u. d. N.}\quad \underbrace{\begin{pmatrix} 1 &amp; 2 \\ 3 &amp; 1 \end{pmatrix}}_{\mathbf{A}} \begin{pmatrix} x_1 \\ x_2\end{pmatrix} \le \begin{pmatrix} 8 \\ 12 \end{pmatrix}</span></p>
<p>Beim Abtippen aus der Tabelle liest er sie spaltenweise statt zeilenweise ein:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>c <span class="op">=</span> np.array([<span class="fl">3.0</span>, <span class="fl">5.0</span>])</span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a>A <span class="op">=</span> np.array([[<span class="fl">1.0</span>, <span class="fl">3.0</span>], <span class="co"># spaltenweise abgetippt statt zeilenweise</span></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a> [<span class="fl">2.0</span>, <span class="fl">1.0</span>]])</span>
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a>b <span class="op">=</span> np.array([<span class="fl">8.0</span>, <span class="fl">12.0</span>])</span>
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a>r <span class="op">=</span> linprog(<span class="op">-</span>c, A_ub<span class="op">=</span>A, b_ub<span class="op">=</span>b, bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> <span class="dv">2</span>)</span>
<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;x =&quot;</span>, np.<span class="bu">round</span>(r.x, <span class="dv">2</span>), <span class="st">&quot; Z =&quot;</span>, <span class="bu">round</span>(<span class="op">-</span>r.fun, <span class="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 <span class="math inline">\mathbf{x}^* = (3{,}2;\ 2{,}4)</span> mit <span class="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 <span class="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><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-fundament">Abschnitt A.2</a>.</em></p>
</blockquote>
</div>
<hr />
<h2 id="sec:fundament-quiz">2.10 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
<p><strong>❓ Micro-Quiz 2: Drei Fragen zum Selbstcheck</strong></p>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <a href="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 <span class="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 <span class="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>
</blockquote>
</div>
<hr />
<h2 id="sec:fundament-selbsttest">2.11 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-fundament">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Was bedeutet der Ausdruck <span class="math inline">\mathbf{c}^\top\mathbf{x}</span> inhaltlich, und was für ein Objekt ist das Ergebnis?</li>
<li>Warum steht in der Technologiematrix eine 0, wenn eine Variable in einer Bedingung nicht vorkommt — warum lässt man den Eintrag nicht weg?</li>
<li>Formulieren Sie den Fundamentalsatz der linearen Optimierung und nennen Sie seinen praktischen Nutzen.</li>
<li>Warum ist ein ganzzahliges Problem nicht konvex, obwohl alle Funktionen darin linear sind?</li>
<li>Ein Kollege sagt: „Mein Optimierer liefert je nach Startwert unterschiedliche Ergebnisse — das ist wohl ein Bug.“ Was antworten Sie?</li>
</ol>
<hr />
<h2 id="sec:fundament-zusammenfassung">2.12 Zusammenfassung</h2>
<ul>
<li><strong>Matrixform</strong> bündelt beliebig viele Variablen und Bedingungen in drei Zeilen: <span class="math inline">\min \mathbf{c}^\top\mathbf{x}</span> u. d. N. <span class="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span>, <span class="math inline">\mathbf{x} \ge \mathbf{0}</span>. Jede Zeile von <span class="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: <span class="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 <span class="math inline">\ge 0</span> sein.</li>
</ul>
<p><strong>Ausblick.</strong> <a href="oekosystem.html#kap-oekosystem">Kapitel 3</a> ordnet das Python-Ökosystem: Welche Bibliothek löst welche Problemklasse, und warum gibt es überhaupt mehrere?</p>
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="fundament.html"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg> <span>Als gelesen markieren</span></button>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="einfuehrung.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a><a class="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><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
</main>
</div>
<footer class="site-footer">
<p>© Dieter Schlüter · <a href="gesamtdokument.html">Gesamtdokument</a> ·
<a href="programme.html">Beispielprogramme</a></p>
</footer>
<script defer src="assets/search-index.js"></script>
<script defer src="assets/programme.js"></script>
<script defer src="assets/site.js"></script>
</body>
</html>