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>
This commit is contained in:
dschlueter 2026-09-08 01:20:09 +02:00
commit b7af2f1d9a
468 changed files with 262141 additions and 0 deletions

View file

@ -0,0 +1,159 @@
#!/usr/bin/env python3
# erzeuge_architektur_diagramme.py
"""
Erzeugt die beiden Architekturdiagramme fuer den Weg vom Skript zum Betrieb:
bilder_04/kap_praxisfallen_schichten.svg / .png Schichten von or_kern.py
bilder_04/kap_testing_dienst.svg / .png der Dienst und sein Ablauf
Beide bilden ab, was in den Programmen tatsaechlich steht - nicht ein
allgemeines Architekturschema:
Schichten - die sechs Abschnitte von or_kern.py, in der Reihenfolge ihrer
Abhaengigkeiten. Die Aussage steckt in der Pfeilrichtung: Alles
zeigt nach innen auf die Domaene, und die Domaene zeigt auf
nichts. Genau deshalb laesst sich der Solver austauschen, ohne
das Fachmodell anzufassen.
Dienst - der Ablauf in Optimierungsdienst.py. Die Aussage ist die
202-Antwort: Wer eine Optimierung synchron beantwortet, baut
einen Dienst, der unter Last umkippt.
Beide entstehen mit Graphviz, in derselben Palette wie die
Entscheidungsdiagramme (siehe erzeuge_solverwahl_diagramme.py).
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_architektur_diagramme.py
Benoetigt: graphviz (Python-Paket) und das Kommandozeilenwerkzeug 'dot'
"""
from __future__ import annotations
import os
import graphviz
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, CYAN, VIOLETT, GRUEN, AMBER = (
"#4338ca", "#0891b2", "#7c3aed", "#059669", "#b45309")
TINT = {INDIGO: "#eef2ff", CYAN: "#ecfeff", VIOLETT: "#f5f3ff",
GRUEN: "#ecfdf5", AMBER: "#fffbeb"}
def neuer_graph(titel: str, richtung: str = "TB") -> graphviz.Digraph:
punkt = graphviz.Digraph(format="svg")
punkt.attr(rankdir=richtung, splines="polyline", nodesep="0.35",
ranksep="0.5", fontname="DejaVu Sans", label=titel, labelloc="t",
fontsize="13", bgcolor="transparent")
punkt.attr("node", fontname="DejaVu Sans", fontsize="10")
punkt.attr("edge", fontname="DejaVu Sans", fontsize="9", color="#5c6380")
return punkt
def kasten(punkt: graphviz.Digraph, name: str, text: str, farbe: str,
form: str = "box") -> None:
punkt.node(name, text, shape=form, style="filled,rounded",
fillcolor=TINT[farbe], color=farbe, penwidth="2.0",
margin="0.16,0.09")
def schreibe(punkt: graphviz.Digraph, name: str) -> None:
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
punkt.format = endung
pfad = punkt.render(os.path.join(BILDER, name), cleanup=True)
print(f"geschrieben: {pfad}")
# --- 1. Die Schichten von or_kern.py ----------------------------------------
def schichten() -> None:
punkt = neuer_graph("Die Schichten von or_kern.py — alle Pfeile zeigen nach innen")
kasten(punkt, "io", "Ein- und Ausgabe\n"
"lade_produktionsproblem() · schreibe_ergebnis()\n"
"Excel, CSV — die Wirklichkeit des Anwenders", AMBER)
kasten(punkt, "bau", "Modellbauer — austauschbar\n"
"loese_mit_glop() · loese_mit_scipy()\n"
"je Solver einer, gleiche Signatur", VIOLETT)
kasten(punkt, "pruef", "Abnahmeprüfung\npruefe_loesung()\n"
"prüft die Lösung OHNE Solver", GRUEN)
kasten(punkt, "erg", "Ergebnis — solverunabhängig\n"
"SolverStatus · Loesung\n"
"fünf Bibliotheken, eine Sprache", CYAN)
kasten(punkt, "dom", "Domäne\nProdukt · Produktionsproblem\n"
"validiert (Pydantic), kennt keinen Solver", INDIGO)
punkt.edge("io", "dom", label="liest ein, schreibt zurück")
punkt.edge("bau", "dom", label="baut das Modell aus")
punkt.edge("bau", "erg", label="liefert")
punkt.edge("pruef", "dom", label="prüft gegen")
punkt.edge("pruef", "erg", label="prüft")
punkt.edge("erg", "dom", style="invis") # haelt die Ebenen sauber
with punkt.subgraph() as gleich:
gleich.attr(rank="same")
gleich.node("bau")
gleich.node("pruef")
punkt.node("regel",
"Die Domäne importiert nichts von außen.\\l"
"Deshalb kostet ein Solverwechsel eine Funktion —\\l"
"und kein einziges Fachdetail.\\l",
shape="note", style="filled", fillcolor="#f8fafc",
color="#94a3b8", fontsize="9", margin="0.18,0.12")
punkt.edge("dom", "regel", style="dashed", color="#94a3b8", arrowhead="none")
schreibe(punkt, "kap_praxisfallen_schichten")
# --- 2. Der Optimierungsdienst ----------------------------------------------
def dienst() -> None:
punkt = neuer_graph("Optimierungsdienst: warum die Antwort 202 lautet, nicht 200")
kasten(punkt, "klient", "Aufrufer\n(curl, Fachanwendung)", CYAN)
kasten(punkt, "post", "POST /plaene\nJSON mit dem Problem", INDIGO)
kasten(punkt, "valid", "Pydantic prüft\ndie Eingabe", VIOLETT, form="diamond")
kasten(punkt, "fehler", "422\nFehler benannt,\nbevor ein Solver läuft", AMBER)
kasten(punkt, "an", "202 Accepted\n+ Auftrags-ID", GRUEN)
kasten(punkt, "rechne", "Hintergrundrechnung\nrechne() → or_kern", VIOLETT)
kasten(punkt, "get", "GET /plaene/{id}\nlaeuft · fertig · fehlgeschlagen", INDIGO)
kasten(punkt, "gesund", "GET /gesundheit\nfür die Überwachung", CYAN)
punkt.edge("klient", "post")
punkt.edge("post", "valid")
punkt.edge("valid", "fehler", label="ungültig")
punkt.edge("valid", "an", label="gültig")
punkt.edge("an", "rechne", label="startet")
# constraint=false: Die Rueckmeldung darf die Rangfolge nicht umsortieren,
# sonst wandert der Aufrufer unter den Dienst.
punkt.edge("an", "klient", label="sofort zurück", style="dashed",
constraint="false")
punkt.edge("klient", "get", label="fragt nach")
punkt.edge("rechne", "get", label="hinterlegt das Ergebnis", style="dashed")
punkt.edge("klient", "gesund", style="dotted")
with punkt.subgraph() as gleich:
gleich.attr(rank="same")
gleich.node("fehler")
gleich.node("an")
punkt.node("regel",
"Eine Optimierung dauert Sekunden bis Minuten.\\l"
"Synchron beantwortet, blockiert sie den Dienst\\l"
"und läuft in jedes Gateway-Timeout.\\l",
shape="note", style="filled", fillcolor="#f8fafc",
color="#94a3b8", fontsize="9", margin="0.18,0.12")
punkt.edge("rechne", "regel", style="dashed", color="#94a3b8",
arrowhead="none")
schreibe(punkt, "kap_testing_dienst")
if __name__ == "__main__":
schichten()
dienst()

View file

@ -0,0 +1,192 @@
#!/usr/bin/env python3
# erzeuge_branch_and_bound.py
"""
Erzeugt den Branch-and-Bound-Suchbaum zum MILP-Kapitel:
bilder_04/kap_milp_suchbaum.svg / .png
Gezeichnet wird der Baum der Handrechnung des Kapitels:
max 5 x1 + 4 x2 u.d.N. 6 x1 + 4 x2 <= 24, x1 + 2 x2 <= 6, x ganzzahlig
Der Baum wird NICHT abgeschrieben, sondern gerechnet: Eine kleine
Branch-and-Bound-Schleife loest die LP-Relaxationen mit scipy.linprog,
verzweigt ueber die erste gebrochene Variable und kappt nach denselben drei
Regeln, die der Kapiteltext nennt (unzulaessig, Schranke schlechter als der
Incumbent, Loesung bereits ganzzahlig). Am Ende prueft das Skript, dass Optimum
und Zahl der geloesten LPs mit dem Buchtext uebereinstimmen - stimmt etwas
nicht, bricht es ab, statt ein plausibles Bild zu malen.
Warum ueberhaupt neu: Das Vorgaengerbild zeigte einen Baum mit voellig anderen
Zahlen (Wurzel x1 = 2,7, Schranke 150,4, Incumbent 138) zu einem Modell, das
nirgends im Buch steht.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_branch_and_bound.py
Benoetigt: numpy, scipy, graphviz (Python-Paket) und das Werkzeug 'dot'
"""
from __future__ import annotations
import os
import graphviz
import numpy as np
from scipy.optimize import linprog
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, CYAN, VIOLETT, GRUEN, AMBER = (
"#4338ca", "#0891b2", "#7c3aed", "#059669", "#b45309")
TINT = {INDIGO: "#eef2ff", CYAN: "#ecfeff", VIOLETT: "#f5f3ff",
GRUEN: "#ecfdf5", AMBER: "#fffbeb"}
# Modell der Handrechnung
C = np.array([5.0, 4.0])
A = np.array([[6.0, 4.0], [1.0, 2.0]])
B = np.array([24.0, 6.0])
TOL = 1e-7
def loese(untergrenzen, obergrenzen):
"""LP-Relaxation mit zusaetzlichen Schranken. Liefert (x, Z) oder (None, None)."""
ergebnis = linprog(c=-C, A_ub=A, b_ub=B,
bounds=list(zip(untergrenzen, obergrenzen)),
method="highs")
if not ergebnis.success:
return None, None
return np.asarray(ergebnis.x), float(-ergebnis.fun)
def erste_gebrochene(x):
for j, wert in enumerate(x):
if abs(wert - round(wert)) > 1e-6:
return j
return None
def zahl(wert: float) -> str:
"""Deutsche Schreibweise, hoechstens zwei Nachkommastellen.
Das '+ 0.0' faengt die negative Null ab: linprog liefert fuer x2 = 0
gelegentlich -0.0, und '(4; -0)' im Buch waere ein Schoenheitsfehler mit
Ansage.
"""
text = f"{wert + 0.0:.2f}".rstrip("0").rstrip(".")
if text in ("-0", "-"):
text = "0"
return text.replace(".", ",")
def branch_and_bound():
"""Baut den Suchbaum und liefert (Knotenliste, Kanten, Optimum, Zielwert).
Die Reihenfolge - erst der Ast mit der kleineren Obergrenze, dann der
andere - entspricht der Handrechnung: Ast A (x2 <= 1) wird vor Ast B
(x2 >= 2) bearbeitet, weshalb der Incumbent aus A stammt und B daran
scheitert.
"""
knoten, kanten = [], []
incumbent, incumbent_x = -np.inf, None
# TIEFENSUCHE (Stapel, nicht Warteschlange). Das ist keine Kosmetik: Nur so
# ist Ast A samt seinen Blaettern fertig, bevor Ast B an die Reihe kommt -
# und nur deshalb scheitert B am Incumbent 20, wie es der Kapiteltext
# beschreibt. Breitensuche wuerde B zum Incumbent machen und die Aussage
# des Bildes umdrehen.
stapel = [("P₀", None, [0.0, 0.0], [np.inf, np.inf], None)]
zaehler = 0
while stapel:
name, elter, unten, oben, kantentext = stapel.pop()
x, z = loese(unten, oben)
zaehler += 1
if elter is not None:
kanten.append((elter, name, kantentext))
if x is None:
knoten.append((name, "unzulässig — gekappt", AMBER, None))
continue
if z < incumbent - TOL:
knoten.append((name, f"Z = {zahl(z)} ≤ Incumbent {zahl(incumbent)}\\n"
"→ gekappt", AMBER, None))
continue
j = erste_gebrochene(x)
if j is None:
besser = z > incumbent + TOL
if besser:
incumbent, incumbent_x = z, x
knoten.append((name,
f"x = ({zahl(x[0])}; {zahl(x[1])}), Z = {zahl(z)}\\n"
+ ("ganzzahlig — neuer Incumbent" if besser
else f"ganzzahlig, aber ≤ Incumbent {zahl(incumbent)}"),
GRUEN if besser else AMBER, z))
continue
knoten.append((name,
f"x = ({zahl(x[0])}; {zahl(x[1])}), Schranke Z ≤ {zahl(z)}\\n"
f"x{j + 1} gebrochen — verzweigen", INDIGO if elter is None else CYAN,
z))
grenze = float(np.floor(x[j]))
links_oben = list(oben); links_oben[j] = grenze
rechts_unten = list(unten); rechts_unten[j] = grenze + 1
# Zuerst den ">="-Ast auflegen, damit der "<="-Ast oben liegt und
# als Erster bearbeitet wird - die Reihenfolge der Handrechnung.
# Namen wie im Kapiteltext: die Wurzel verzweigt in Ast A und Ast B,
# deren Kinder heissen A1/A2 bzw. B1/B2.
links, rechts = ("A", "B") if elter is None else (f"{name}1", f"{name}2")
stapel.append((rechts, name, rechts_unten, list(oben),
f"x{j + 1}{int(grenze) + 1}"))
stapel.append((links, name, list(unten), links_oben,
f"x{j + 1}{int(grenze)}"))
return knoten, kanten, incumbent_x, incumbent, zaehler
def zeichne(knoten, kanten, bestes, zielwert, geloest) -> None:
punkt = graphviz.Digraph(format="svg")
punkt.attr(rankdir="TB", splines="polyline", nodesep="0.45", ranksep="0.55",
fontname="DejaVu Sans", bgcolor="transparent", fontsize="12",
labelloc="t",
label=(f"Branch-and-Bound: {geloest} gelöste LPs statt aller "
f"ganzzahligen Punkte\\n"
f"max 5x₁ + 4x₂ u.d.N. 6x₁+4x₂ ≤ 24, x₁+2x₂ ≤ 6"))
punkt.attr("node", fontname="DejaVu Sans", fontsize="10")
punkt.attr("edge", fontname="DejaVu Sans", fontsize="9", color="#5c6380")
for name, text, farbe, _ in knoten:
beschriftung = f"{name}\\n{text}"
punkt.node(name, beschriftung, shape="box", style="filled,rounded",
fillcolor=TINT[farbe], color=farbe, penwidth="2.0",
margin="0.16,0.09")
for von, nach, text in kanten:
punkt.edge(von, nach, label=text or "")
punkt.node("ergebnis",
f"Optimum: x* = ({zahl(bestes[0])}; {zahl(bestes[1])}), "
f"Z* = {zahl(zielwert)}",
shape="note", style="filled", fillcolor="#f8fafc",
color="#94a3b8", fontsize="10", margin="0.18,0.12")
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
punkt.format = endung
pfad = punkt.render(os.path.join(BILDER, "kap_milp_suchbaum"), cleanup=True)
print(f"geschrieben: {pfad}")
if __name__ == "__main__":
knoten, kanten, bestes, zielwert, geloest = branch_and_bound()
print(f"{len(knoten)} Knoten, {geloest} geloeste LPs")
for name, text, _, _ in knoten:
print(f" {name:5} {text.replace(chr(92) + 'n', ' | ')}")
print(f"Optimum: ({bestes[0]:g}; {bestes[1]:g}), Z = {zielwert:g}")
# Gegenprobe gegen die Handrechnung im Kapitel
if not (np.allclose(bestes, [4.0, 0.0]) and abs(zielwert - 20.0) < TOL):
raise SystemExit("FEHLER: Ergebnis weicht von der Handrechnung ab "
"(erwartet x* = (4; 0), Z* = 20).")
zeichne(knoten, kanten, bestes, zielwert, geloest)

View file

@ -0,0 +1,275 @@
#!/usr/bin/env python3
# erzeuge_effizienzlinie.py
"""
Erzeugt die Figur "Was die Hausregeln kosten" zum Kapitel Markowitz:
bilder_04/kap_markowitz_restriktionen.svg statisch, fuer das PDF
bilder_04/kap_markowitz_restriktionen.png Rasterfassung
bilder_04/plotly/kap_markowitz_restriktionen.html interaktiv, fuer die Website
Gezeigt werden zwei Effizienzlinien desselben Universums: einmal ohne
Beschraenkungen, einmal mit den institutionellen Regeln aus dem Kapitel
(hoechstens 20 % je Titel, Sektorgrenzen). Der senkrechte Abstand zwischen
beiden Kurven IST der Preis der Hausregeln - in Renditepunkten, bei gleichem
Risiko.
Warum eine feste Instanz und nicht die Kursdaten aus Markowitz_CVXPY.py:
Jenes Programm laedt Livedaten. Ein Diagramm daraus saehe an jedem Tag anders
aus, und der abgedruckte Text koennte nicht dazu passen. Hier wird deshalb ein
FESTES Universum aus einem Faktormodell erzeugt (Marktfaktor, vier
Sektorfaktoren, titelspezifisches Rauschen) - dieselbe Struktur wie im Kapitel,
dieselben Regeln, aber reproduzierbar. Die Zahlen des Livelaufs weichen ab, die
Aussage nicht.
Die interaktive Fassung zeigt im Hover, was das statische Bild nicht kann: die
GEWICHTE des jeweiligen Portfolios. Damit wird sichtbar, wie sich die
Zusammensetzung entlang der Linie verschiebt - und woran die beschraenkte
Fassung scheitert, sobald ein einzelner Titel an seine 20-%-Grenze stoesst.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_effizienzlinie.py
Benoetigt: numpy, cvxpy, matplotlib, plotly
"""
from __future__ import annotations
import os
import cvxpy as cp
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import plotly.graph_objects as go
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
PLOTLY_VERZ = os.path.join(BILDER, "plotly")
INDIGO, CYAN, AMBER, GRUEN = "#4338ca", "#0891b2", "#b45309", "#059669"
# Struktur wie in Markowitz_CVXPY.py - zehn Titel in vier Sektoren
TITEL = ["AAPL", "MSFT", "NVDA", "AMZN", "JNJ", "PFE", "JPM", "GS", "XOM", "CVX"]
SEKTOR_VON = ["Technologie"] * 4 + ["Gesundheit"] * 2 + ["Finanzen"] * 2 + ["Energie"] * 2
SEKTORGRENZEN = {"Technologie": 0.35, "Gesundheit": 0.40,
"Finanzen": 0.40, "Energie": 0.40}
MAX_GEWICHT = 0.20
PUNKTE = 40
def universum(saat: int = 3) -> tuple[np.ndarray, np.ndarray]:
"""Erwartete Renditen und Kovarianzmatrix aus einem Faktormodell.
Jeder Titel haengt am Markt, an seinem Sektor und an eigenem Rauschen.
Genau diese Struktur macht Diversifikation ueberhaupt moeglich: Titel
desselben Sektors laufen staerker im Gleichschritt als Titel verschiedener
Sektoren - und nur der zweite Fall traegt.
"""
rng = np.random.default_rng(saat)
n = len(TITEL)
sektoren = sorted(set(SEKTOR_VON))
ladung_markt = rng.uniform(0.7, 1.3, n)
ladung_sektor = rng.uniform(0.4, 0.9, n)
eigen = rng.uniform(0.10, 0.26, n)
sigma_markt, sigma_sektor = 0.16, 0.12
kovarianz = np.outer(ladung_markt, ladung_markt) * sigma_markt ** 2
for sektor in sektoren:
maske = np.array([s == sektor for s in SEKTOR_VON], dtype=float)
gewicht = ladung_sektor * maske
kovarianz += np.outer(gewicht, gewicht) * sigma_sektor ** 2
kovarianz += np.diag(eigen ** 2)
# Erwartete Renditen: Marktprämie plus etwas titelspezifische Streuung
mu = 0.03 + 0.055 * ladung_markt + rng.normal(0, 0.012, n)
return mu, kovarianz
def sektormasken() -> dict[str, np.ndarray]:
return {sektor: np.array([s == sektor for s in SEKTOR_VON], dtype=float)
for sektor in SEKTORGRENZEN}
def linie(mu: np.ndarray, kovarianz: np.ndarray, beschraenkt: bool):
"""Effizienzlinie ueber das epsilon-Constraint-Verfahren.
Fuer jede Zielrendite wird das minimale Risiko gesucht. Das ist dasselbe
Verfahren, mit dem im Kapitel Mehrziel die Pareto-Front entsteht - eine
Effizienzlinie IST eine Pareto-Front aus Rendite und Risiko.
"""
n = len(mu)
w = cp.Variable(n, nonneg=True)
ziel_rendite = cp.Parameter(nonneg=True)
# Cholesky statt quad_form: die 2-Norm eines affinen Ausdrucks ist
# zweifelsfrei DCP (siehe die Fehlerdiagnose zu DCPError).
wurzel = np.linalg.cholesky(kovarianz + 1e-12 * np.eye(n))
bedingungen = [cp.sum(w) == 1, mu @ w >= ziel_rendite]
if beschraenkt:
bedingungen.append(w <= MAX_GEWICHT)
for sektor, maske in sektormasken().items():
bedingungen.append(maske @ w <= SEKTORGRENZEN[sektor])
problem = cp.Problem(cp.Minimize(cp.norm2(wurzel.T @ w)), bedingungen)
# Der obere Rand ist NICHT max(mu): Unter Restriktionen ist der beste
# Einzeltitel gar nicht voll kaufbar. Also wird er ausgerechnet.
w_max = cp.Variable(n, nonneg=True)
obere = [cp.sum(w_max) == 1]
if beschraenkt:
obere.append(w_max <= MAX_GEWICHT)
for sektor, maske in sektormasken().items():
obere.append(maske @ w_max <= SEKTORGRENZEN[sektor])
cp.Problem(cp.Maximize(mu @ w_max), obere).solve()
hoechste = float(mu @ w_max.value)
# Untere Grenze: die Rendite des Minimum-Varianz-Portfolios
ziel_rendite.value = 0.0
problem.solve()
kleinste = float(mu @ w.value)
punkte = []
for ziel in np.linspace(kleinste, hoechste, PUNKTE):
ziel_rendite.value = float(ziel)
problem.solve()
if problem.status not in (cp.OPTIMAL, cp.OPTIMAL_INACCURATE):
continue
gewichte = np.asarray(w.value).ravel()
risiko = float(np.sqrt(gewichte @ kovarianz @ gewichte))
punkte.append((risiko, float(mu @ gewichte), gewichte))
return punkte
def aufpreis(frei, gebunden) -> tuple[float, float]:
"""Groesster senkrechter Abstand: Wo kosten die Regeln am meisten Rendite?
Verglichen wird bei GLEICHEM Risiko - alles andere waere kein Preis,
sondern nur ein anderer Punkt auf der Kurve.
"""
risiken_frei = np.array([p[0] for p in frei])
renditen_frei = np.array([p[1] for p in frei])
bester_abstand, bestes_risiko = 0.0, 0.0
for risiko, rendite, _ in gebunden:
if risiko < risiken_frei.min() or risiko > risiken_frei.max():
continue
vergleich = float(np.interp(risiko, risiken_frei, renditen_frei))
if vergleich - rendite > bester_abstand:
bester_abstand, bestes_risiko = vergleich - rendite, risiko
return bester_abstand, bestes_risiko
def zeichne_statisch(frei, gebunden, abstand, risiko_stelle) -> None:
figur, achse = plt.subplots(figsize=(7.4, 4.6))
for punkte, farbe, name, stil in (
(frei, INDIGO, "ohne Beschränkungen", "-"),
(gebunden, AMBER, "mit Hausregeln (20 % je Titel, Sektorgrenzen)", "-")):
x = [p[0] * 100 for p in punkte]
y = [p[1] * 100 for p in punkte]
achse.plot(x, y, stil, color=farbe, linewidth=2.0, label=name)
if abstand > 0:
oben = float(np.interp(risiko_stelle,
[p[0] for p in frei], [p[1] for p in frei]))
unten = oben - abstand
achse.annotate("", xy=(risiko_stelle * 100, oben * 100),
xytext=(risiko_stelle * 100, unten * 100),
arrowprops=dict(arrowstyle="<->", color=GRUEN, lw=1.8))
text = f"{abstand * 100:.2f} Prozentpunkte".replace(".", ",")
achse.annotate(f"{text}\nRendite, bei gleichem Risiko",
(risiko_stelle * 100, (oben + unten) / 2 * 100),
textcoords="offset points", xytext=(10, 0),
fontsize=8.5, color=GRUEN, va="center")
# Der teuerste Effekt ist nicht der senkrechte Abstand, sondern die Decke:
# Die beschraenkte Linie endet frueher. Alles rechts davon ist mit den
# Hausregeln ueberhaupt nicht erreichbar - egal, wie viel Risiko man traegt.
ende_gebunden, ende_frei = gebunden[-1], frei[-1]
achse.scatter([ende_gebunden[0] * 100], [ende_gebunden[1] * 100], s=110,
facecolors="none", edgecolors=AMBER, linewidths=2.2, zorder=5)
verlust = (ende_frei[1] - ende_gebunden[1]) * 100
achse.annotate(f"hier ist Schluss: {ende_gebunden[1] * 100:.2f} % statt "
f"{ende_frei[1] * 100:.2f} %\n"
f"{verlust:.2f} Punkte, die kein Risiko mehr kauft".replace(".", ","),
(ende_gebunden[0] * 100, ende_gebunden[1] * 100),
textcoords="offset points", xytext=(8, -22),
fontsize=8.5, color=AMBER)
achse.set_xlabel("Volatilität $\\sigma$ (% p. a.)")
achse.set_ylabel("erwartete Rendite $\\mu$ (% p. a.)")
achse.set_title("Was die Hausregeln kosten", fontsize=11)
achse.grid(linestyle=":", alpha=0.5)
achse.legend(fontsize=8.5, loc="lower right", frameon=False)
for rand in ("top", "right"):
achse.spines[rand].set_visible(False)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap_markowitz_restriktionen.{endung}")
figur.savefig(pfad, format=endung, dpi=160,
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)
def zeichne_interaktiv(frei, gebunden) -> None:
figur = go.Figure()
def beschriftung(punkt):
risiko, rendite, gewichte = punkt
reihenfolge = np.argsort(-gewichte)[:4]
anteile = "<br>".join(
f"&nbsp;&nbsp;{TITEL[i]} ({SEKTOR_VON[i]}): {gewichte[i] * 100:.1f} %"
for i in reihenfolge if gewichte[i] > 0.005)
sektoren = "<br>".join(
f"&nbsp;&nbsp;{sektor}: {float(maske @ gewichte) * 100:.1f} %"
for sektor, maske in sektormasken().items())
return (f"<b>{rendite * 100:.2f} % Rendite</b><br>"
f"{risiko * 100:.2f} % Volatilität<br>"
f"Sharpe {(rendite - 0.03) / risiko:.2f}<br><br>"
f"<b>größte Positionen</b><br>{anteile}<br><br>"
f"<b>je Sektor</b><br>{sektoren}<extra></extra>")
for punkte, farbe, name in ((frei, INDIGO, "ohne Beschränkungen"),
(gebunden, AMBER, "mit Hausregeln")):
figur.add_trace(go.Scatter(
x=[p[0] * 100 for p in punkte], y=[p[1] * 100 for p in punkte],
mode="lines+markers", name=name,
line=dict(color=farbe, width=2.2), marker=dict(size=6),
hovertemplate=[beschriftung(p) for p in punkte]))
figur.update_layout(
title="Effizienzlinie mit und ohne Hausregeln — Gewichte im Hover",
xaxis=dict(title="Volatilität σ (% p. a.)"),
yaxis=dict(title="erwartete Rendite μ (% p. a.)"),
height=460, margin=dict(l=70, r=20, t=55, b=50),
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
legend=dict(orientation="h", y=-0.18), hovermode="closest")
os.makedirs(PLOTLY_VERZ, exist_ok=True)
pfad = os.path.join(PLOTLY_VERZ, "kap_markowitz_restriktionen.html")
with open(pfad, "w", encoding="utf-8") as datei:
datei.write(figur.to_html(full_html=False, include_plotlyjs=False,
div_id="kap-markowitz-restriktionen"))
print(f"geschrieben: {pfad}")
if __name__ == "__main__":
mu, kovarianz = universum()
frei = linie(mu, kovarianz, beschraenkt=False)
gebunden = linie(mu, kovarianz, beschraenkt=True)
abstand, stelle = aufpreis(frei, gebunden)
print(f"frei: {len(frei)} Punkte, Rendite "
f"{frei[0][1] * 100:.2f} bis {frei[-1][1] * 100:.2f} %")
print(f"gebunden: {len(gebunden)} Punkte, Rendite "
f"{gebunden[0][1] * 100:.2f} bis {gebunden[-1][1] * 100:.2f} %")
print(f"groesster Abstand: {abstand * 100:.2f} Prozentpunkte "
f"bei sigma = {stelle * 100:.2f} %")
zeichne_statisch(frei, gebunden, abstand, stelle)
zeichne_interaktiv(frei, gebunden)

View file

@ -0,0 +1,170 @@
#!/usr/bin/env python3
# erzeuge_kap06_gantt.py
"""
Erzeugt das Gantt-Diagramm zum Job-Shop-Beispiel aus Kapitel 6 in zwei Fassungen:
bilder_04/kap06_jobshop_gantt.svg statisch, fuer das PDF
bilder_04/kap06_jobshop_gantt.png statisch, Rasterfassung
bilder_04/plotly/kap06_jobshop_gantt.html interaktiv, fuer die Website
Die Daten sind identisch mit JobShop_Intervalle.py; geloest wird hier erneut,
damit Diagramm und Buchtext nicht auseinanderlaufen koennen.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_kap06_gantt.py
Benoetigt: ortools, matplotlib, plotly
"""
from __future__ import annotations
import os
import matplotlib
matplotlib.use("Agg") # kein Fenster, auch auf Servern
import matplotlib.pyplot as plt
import plotly.graph_objects as go
from ortools.sat.python import cp_model
# Identisch zu JobShop_Intervalle.py
AUFTRAEGE = [
[(0, 3), (1, 2), (2, 2)], # Auftrag 0
[(0, 2), (2, 1), (1, 4)], # Auftrag 1
[(1, 4), (2, 3)], # Auftrag 2
]
MASCHINENNAMEN = ["Fraese", "Dreherei", "Lackiererei"]
# Buchpalette (Indigo, Cyan, Amber) - dieselben Farben wie im uebrigen Werk
FARBEN = ["#4338ca", "#0891b2", "#b45309"]
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
PLOTLY_VERZ = os.path.join(BILDER, "plotly")
def loese() -> tuple[list[tuple[int, int, int, int]], int]:
"""Liefert (Auftrag, Maschine, Beginn, Dauer) je Arbeitsgang und den Makespan."""
anzahl_maschinen = 1 + max(m for auftrag in AUFTRAEGE for m, _ in auftrag)
horizont = sum(dauer for auftrag in AUFTRAEGE for _, dauer in auftrag)
modell = cp_model.CpModel()
intervalle: dict[int, list] = {m: [] for m in range(anzahl_maschinen)}
beginn_var: dict[tuple[int, int], object] = {}
ende_var: dict[tuple[int, int], object] = {}
for a, auftrag in enumerate(AUFTRAEGE):
for g, (maschine, dauer) in enumerate(auftrag):
beginn = modell.NewIntVar(0, horizont, f"b_{a}_{g}")
ende = modell.NewIntVar(0, horizont, f"e_{a}_{g}")
intervall = modell.NewIntervalVar(beginn, dauer, ende, f"i_{a}_{g}")
intervalle[maschine].append(intervall)
beginn_var[a, g] = beginn
ende_var[a, g] = ende
if g > 0: # Arbeitsgaenge in ihrer Reihenfolge
modell.Add(beginn >= ende_var[a, g - 1])
for maschine in range(anzahl_maschinen):
modell.AddNoOverlap(intervalle[maschine])
makespan = modell.NewIntVar(0, horizont, "makespan")
modell.AddMaxEquality(makespan, [ende_var[a, len(auftrag) - 1]
for a, auftrag in enumerate(AUFTRAEGE)])
modell.Minimize(makespan)
loeser = cp_model.CpSolver()
# Dieselben Parameter wie in JobShop_Intervalle.py - sonst zeigt das
# Diagramm einen anderen (gleich guten) Plan als der Buchtext.
loeser.parameters.num_workers = 1
loeser.parameters.random_seed = 1
status = loeser.Solve(modell)
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
raise RuntimeError(f"Kein Plan gefunden: {loeser.StatusName(status)}")
plan = [(a, maschine, loeser.Value(beginn_var[a, g]), dauer)
for a, auftrag in enumerate(AUFTRAEGE)
for g, (maschine, dauer) in enumerate(auftrag)]
return plan, loeser.Value(makespan)
def zeichne_statisch(plan, makespan: int) -> None:
"""Matplotlib-Fassung fuer das PDF."""
figur, achse = plt.subplots(figsize=(9.0, 3.2))
for auftrag, maschine, beginn, dauer in plan:
achse.barh(y=maschine, width=dauer, left=beginn, height=0.55,
color=FARBEN[auftrag % len(FARBEN)],
edgecolor="white", linewidth=1.2)
achse.text(beginn + dauer / 2, maschine, f"A{auftrag}",
ha="center", va="center", color="white",
fontsize=10, fontweight="bold")
achse.set_yticks(range(len(MASCHINENNAMEN)))
achse.set_yticklabels(MASCHINENNAMEN)
achse.invert_yaxis()
achse.set_xlabel("Zeiteinheiten")
achse.set_xlim(0, makespan)
achse.set_xticks(range(makespan + 1))
achse.set_title(f"Job-Shop-Plan, Makespan = {makespan}", fontsize=11)
achse.grid(axis="x", linestyle=":", alpha=0.5)
for rand in ("top", "right", "left"):
achse.spines[rand].set_visible(False)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap06_jobshop_gantt.{endung}")
figur.savefig(pfad, format=endung, dpi=160)
print(f"geschrieben: {pfad}")
plt.close(figur)
def zeichne_interaktiv(plan, makespan: int) -> None:
"""Plotly-Fassung fuer die Website.
include_plotlyjs=False: Die Bibliothek wird vom Build einmal je Seite
eingebunden (assets/plotly.min.js), nicht in jede Figur kopiert.
"""
figur = go.Figure()
schon_beschriftet = set()
for auftrag, maschine, beginn, dauer in plan:
figur.add_trace(go.Bar(
x=[dauer], y=[MASCHINENNAMEN[maschine]], base=[beginn],
orientation="h", name=f"Auftrag {auftrag}",
legendgroup=f"A{auftrag}",
showlegend=auftrag not in schon_beschriftet,
marker_color=FARBEN[auftrag % len(FARBEN)],
marker_line=dict(color="white", width=1.5),
hovertemplate=(f"<b>Auftrag {auftrag}</b><br>"
f"{MASCHINENNAMEN[maschine]}<br>"
f"von {beginn} bis {beginn + dauer} "
f"({dauer} Zeiteinheiten)<extra></extra>"),
))
schon_beschriftet.add(auftrag)
figur.update_layout(
barmode="overlay",
title=f"Job-Shop-Plan, Makespan = {makespan}",
xaxis=dict(title="Zeiteinheiten", dtick=1, range=[0, makespan]),
yaxis=dict(autorange="reversed"),
height=320,
margin=dict(l=90, r=20, t=50, b=45),
# Transparenter Hintergrund, damit die Figur sich in helles wie
# dunkles Seitendesign einfuegt.
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
legend=dict(orientation="h", y=-0.25),
)
os.makedirs(PLOTLY_VERZ, exist_ok=True)
pfad = os.path.join(PLOTLY_VERZ, "kap06_jobshop_gantt.html")
with open(pfad, "w", encoding="utf-8") as datei:
datei.write(figur.to_html(full_html=False, include_plotlyjs=False,
div_id="kap06-jobshop-gantt"))
print(f"geschrieben: {pfad}")
if __name__ == "__main__":
plan, makespan = loese()
print(f"Makespan: {makespan}")
zeichne_statisch(plan, makespan)
zeichne_interaktiv(plan, makespan)

224
bilder_04/erzeuge_kkt.py Normal file
View file

@ -0,0 +1,224 @@
#!/usr/bin/env python3
# erzeuge_kkt.py
"""
Erzeugt das KKT-Diagramm zum QP/NLP-Kapitel:
bilder_04/kap_qp_kkt.svg / .png
Zwei Bilder nebeneinander:
Links - die Handrechnung des Kapitels, eindimensional:
min (x-5)^2 u.d.N. x - 3 <= 0 -> x* = 3, lambda* = 4
Sichtbar wird, was lambda BEDEUTET: Es ist die Steigung von f am
Rand, also der Preis dafuer, dass die Grenze nicht weiter darf.
Rechts - dieselbe Aussage in zwei Dimensionen: Am Optimum zeigen der
Zielgradient und der mit lambda gewichtete Restriktionsgradient
GENAU GEGENEINANDER und heben sich auf.
Warum neu: Im Vorgaengerbild war der Zielpfeil mit "-grad f" beschriftet und
zeigte entgegen lambda*grad g. Das ist die Konstellation einer MAXIMIERUNG. Das
Kapitel formuliert KKT aber fuer die Minimierung - dort gilt
grad f + lambda*grad g = 0, also zeigen grad f (ohne Minus) und lambda*grad g
gegeneinander. Eine Beschriftung, ein Vorzeichen, und die Aussage kippt.
Alle Werte werden gerechnet, nicht eingetragen: Das Optimum kommt aus
scipy.optimize, die Gradienten aus der Ableitung, und das Skript prueft alle
vier KKT-Bedingungen numerisch, bevor es zeichnet.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_kkt.py
Benoetigt: numpy, scipy, matplotlib
"""
from __future__ import annotations
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import minimize
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, CYAN, AMBER, GRUEN, ROT = (
"#4338ca", "#0891b2", "#b45309", "#059669", "#be123c")
# --- Eindimensional: die Handrechnung des Kapitels --------------------------
ZIEL_1D = 5.0 # unbeschraenktes Minimum von (x-5)^2
GRENZE_1D = 3.0 # x <= 3
# --- Zweidimensional: dieselbe Struktur, ein Ziel mehr ----------------------
ZENTRUM = np.array([3.5, 3.5]) # unbeschraenktes Minimum
BUDGET = 4.0 # x1 + x2 <= 4
def f(x):
return float(np.sum((np.asarray(x) - ZENTRUM) ** 2))
def grad_f(x):
return 2.0 * (np.asarray(x) - ZENTRUM)
def g(x):
return float(np.sum(x) - BUDGET)
GRAD_G = np.array([1.0, 1.0])
def loese_2d():
ergebnis = minimize(f, x0=np.zeros(2), method="SLSQP",
constraints=[{"type": "ineq", "fun": lambda x: -g(x)}])
if not ergebnis.success:
raise SystemExit(f"SLSQP gescheitert: {ergebnis.message}")
x = np.asarray(ergebnis.x)
# lambda aus der Stationaritaet: grad f + lambda * grad g = 0
lam = float(-(grad_f(x) @ GRAD_G) / (GRAD_G @ GRAD_G))
return x, lam
def pruefe_kkt(x, lam, toleranz=1e-5) -> None:
"""Alle vier Bedingungen, bevor gezeichnet wird - nicht danach."""
stationaritaet = grad_f(x) + lam * GRAD_G
pruefungen = {
"Stationarität grad f + lambda*grad g = 0":
float(np.linalg.norm(stationaritaet)) < toleranz,
"primale Zulässigkeit g(x) <= 0": g(x) < toleranz,
"duale Zulässigkeit lambda >= 0": lam > -toleranz,
"komplementärer Schlupf lambda*g(x) = 0": abs(lam * g(x)) < toleranz,
}
for name, erfuellt in pruefungen.items():
print(f" {'OK ' if erfuellt else 'NEIN'} {name}")
if not all(pruefungen.values()):
raise SystemExit("FEHLER: KKT nicht erfuellt - es wird nichts gezeichnet.")
def zahl(wert: float) -> str:
text = f"{wert:.2f}".rstrip("0").rstrip(".")
return text.replace(".", ",")
def zeichne_1d(achse, lam_1d: float) -> None:
x = np.linspace(0.5, 6.5, 400)
achse.plot(x, (x - ZIEL_1D) ** 2, color=INDIGO, linewidth=2.0,
label="$f(x) = (x-5)^2$")
achse.axvspan(GRENZE_1D, 6.5, color=ROT, alpha=0.07)
achse.axvline(GRENZE_1D, color=ROT, linewidth=1.8, linestyle="--")
achse.annotate("unzulässig:\n$x > 3$", (GRENZE_1D, 20),
textcoords="offset points", xytext=(10, 0),
fontsize=8.5, color=ROT, va="center")
achse.scatter([ZIEL_1D], [0.0], s=90, facecolors="none",
edgecolors="#94a3b8", linewidths=2.0, zorder=4)
achse.annotate("freies Minimum $x=5$\n— nicht erlaubt", (ZIEL_1D, 0.0),
textcoords="offset points", xytext=(10, 16),
ha="left", fontsize=8.5, color="#64748b")
wert = (GRENZE_1D - ZIEL_1D) ** 2
achse.scatter([GRENZE_1D], [wert], s=130, color=GRUEN, zorder=5)
achse.annotate(f"$x^*=3$, $f=4$", (GRENZE_1D, wert),
textcoords="offset points", xytext=(-10, 12),
ha="right", fontsize=9, color=GRUEN, fontweight="bold")
# Die Tangente am Rand: ihre Steigung ist -lambda*
steigung = 2 * (GRENZE_1D - ZIEL_1D)
nahe = np.linspace(GRENZE_1D - 1.2, GRENZE_1D + 0.8, 20)
achse.plot(nahe, wert + steigung * (nahe - GRENZE_1D), color=AMBER,
linewidth=1.6, linestyle="-",
label=f"Steigung $-\\lambda^* = {zahl(steigung)}$")
achse.set_xlabel("$x$")
achse.set_ylabel("$f(x)$")
achse.set_title("Die Handrechnung: $\\lambda^*$ ist die Steigung am Rand",
fontsize=10.5)
achse.set_ylim(-1.5, 22)
achse.grid(linestyle=":", alpha=0.45)
achse.legend(fontsize=8, loc="upper left", frameon=False)
for seite in ("top", "right"):
achse.spines[seite].set_visible(False)
def zeichne_2d(achse, x_stern, lam) -> None:
gitter = np.linspace(-0.2, 5.4, 300)
X1, X2 = np.meshgrid(gitter, gitter)
werte = (X1 - ZENTRUM[0]) ** 2 + (X2 - ZENTRUM[1]) ** 2
achse.contour(X1, X2, werte, levels=[0.5, 2, 4.5, 8, 12.5, 18],
colors=[INDIGO], linewidths=0.8, alpha=0.55)
rand = np.linspace(-0.2, 5.4, 10)
achse.plot(rand, BUDGET - rand, color=ROT, linewidth=2.0,
label="$g(x)=x_1+x_2-4=0$")
achse.fill_between(rand, -0.2, np.minimum(BUDGET - rand, 5.4),
color=CYAN, alpha=0.08)
achse.scatter(*ZENTRUM, s=90, facecolors="none", edgecolors="#94a3b8",
linewidths=2.0, zorder=4)
achse.annotate("freies Minimum\n— unzulässig", ZENTRUM,
textcoords="offset points", xytext=(12, -8),
va="top", fontsize=8.5, color="#64748b")
achse.scatter(*x_stern, s=140, color=GRUEN, zorder=6)
achse.annotate(f"$x^* = ({zahl(x_stern[0])}; {zahl(x_stern[1])})$",
x_stern, textcoords="offset points", xytext=(-12, -20),
ha="right", fontsize=9, color=GRUEN, fontweight="bold")
zielgradient = grad_f(x_stern)
achse.annotate("", xy=x_stern + zielgradient * 0.45, xytext=x_stern,
arrowprops=dict(arrowstyle="-|>", color=INDIGO, lw=2.4))
achse.annotate("$\\nabla f$ — zeigt bergauf,\nweg vom freien Minimum",
x_stern + zielgradient * 0.45,
textcoords="offset points", xytext=(-8, -8),
ha="right", va="top", fontsize=8.5, color=INDIGO)
restriktionsgradient = lam * GRAD_G
achse.annotate("", xy=x_stern + restriktionsgradient * 0.45, xytext=x_stern,
arrowprops=dict(arrowstyle="-|>", color=AMBER, lw=2.4))
achse.annotate(f"$\\lambda^*\\nabla g$ mit $\\lambda^* = {zahl(lam)}$",
x_stern + restriktionsgradient * 0.45,
textcoords="offset points", xytext=(-10, 8),
ha="right", fontsize=8.5, color=AMBER)
achse.set_xlim(-0.2, 5.4)
achse.set_ylim(-0.2, 5.4)
achse.set_xlabel("$x_1$")
achse.set_ylabel("$x_2$")
achse.set_title("Am Optimum heben sich beide Gradienten auf:\n"
"$\\nabla f + \\lambda^*\\nabla g = 0$", fontsize=10.5)
achse.grid(linestyle=":", alpha=0.45)
achse.legend(fontsize=8, loc="upper right", frameon=False)
for seite in ("top", "right"):
achse.spines[seite].set_visible(False)
if __name__ == "__main__":
x_stern, lam = loese_2d()
print(f"2D: x* = ({x_stern[0]:.4f}; {x_stern[1]:.4f}), lambda* = {lam:.4f}")
pruefe_kkt(x_stern, lam)
lam_1d = -2 * (GRENZE_1D - ZIEL_1D)
print(f"1D: x* = {GRENZE_1D:g}, lambda* = {lam_1d:g} "
f"(Handrechnung: 3 und 4)")
if abs(lam_1d - 4.0) > 1e-9:
raise SystemExit("FEHLER: 1D-Lambda weicht von der Handrechnung ab.")
figur, (links, rechts) = plt.subplots(1, 2, figsize=(11.0, 4.8))
zeichne_1d(links, lam_1d)
zeichne_2d(rechts, x_stern, lam)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap_qp_kkt.{endung}")
figur.savefig(pfad, format=endung, dpi=160,
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)

View file

@ -0,0 +1,204 @@
#!/usr/bin/env python3
# erzeuge_kondition.py
"""
Erzeugt das Konditionszahl-Diagramm zum Kapitel Fundament:
bilder_04/kap_fundament_kondition.svg statisch, fuer das PDF
bilder_04/kap_fundament_kondition.png Rasterfassung
Zwei Aussagen, beide gerechnet und nicht behauptet:
Links - kappa(A) ist keine grobe Faustregel, sondern eine SCHARFE obere
Schranke. Die gemessene groesste Fehlerverstaerkung liegt ueber
zwoelf Groessenordnungen hinweg praktisch auf der Geraden y = kappa;
der Median darunter. Wer also mit kappa = 1e8 rechnet, verliert im
schlimmsten Fall genau acht Stellen - nicht ungefaehr, sondern
nachweisbar.
Rechts - Was die Ruiz-Equilibrierung an einem Modell ausrichtet, in dem
Euro-Betraege und Tonnen-Angaben in derselben Matrix stehen.
Verfahren und Instanz sind wortgleich zu Skalierung_Kondition.py, und
gerechnet wird hier erneut - so koennen Diagramm und Buchtext nicht
auseinanderlaufen.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_kondition.py
Benoetigt: numpy, matplotlib
"""
from __future__ import annotations
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, CYAN, AMBER, GRUEN = "#4338ca", "#0891b2", "#b45309", "#059669"
STOERUNG = 1e-10 # relative Stoerung der rechten Seite
GUT = np.array([[2.0, 1.0], [1.0, 3.0]])
SCHLECHT = np.array([[1.0, 1.0], [1.0, 1.0 + 1e-8]])
# --- Messung: wortgleich zu Skalierung_Kondition.py -------------------------
def fehlerverstaerkung(A: np.ndarray, rng: np.random.Generator,
versuche: int = 200) -> tuple[float, float]:
"""Stoert b relativ um STOERUNG und misst den Faktor, um den der
Loesungsfehler groesser ausfaellt. Liefert (Median, Maximum)."""
x_wahr = np.ones(A.shape[1])
b = A @ x_wahr
faktoren = []
for _ in range(versuche):
richtung = rng.normal(size=b.size)
richtung /= np.linalg.norm(richtung)
b_gestoert = b + STOERUNG * np.linalg.norm(b) * richtung
x_gestoert = np.linalg.solve(A, b_gestoert)
rel_x = np.linalg.norm(x_gestoert - x_wahr) / np.linalg.norm(x_wahr)
faktoren.append(rel_x / STOERUNG)
return float(np.median(faktoren)), float(np.max(faktoren))
def ruiz_equilibrierung(A: np.ndarray, durchlaeufe: int = 20) -> np.ndarray:
"""Zeilen- und Spaltenskalierung, bis alle Maxima nahe 1 liegen."""
A_s = A.astype(float).copy()
for _ in range(durchlaeufe):
zeilen_max = np.abs(A_s).max(axis=1)
zeilen_max[zeilen_max == 0] = 1.0
A_s = (1.0 / np.sqrt(zeilen_max))[:, None] * A_s
spalten_max = np.abs(A_s).max(axis=0)
spalten_max[spalten_max == 0] = 1.0
A_s = A_s * (1.0 / np.sqrt(spalten_max))[None, :]
return A_s
def mischmodell() -> np.ndarray:
"""Das Modell mit den vier unvertraeglichen Einheiten.
Skalierung_Kondition.py zieht diese Matrix aus DEMSELBEN Generator, aus dem
zuvor schon die Stoerungen von Experiment 1 kamen. Damit hier bitgleich
dieselbe Matrix entsteht - und das Diagramm dieselbe Konditionszahl zeigt
wie der abgedruckte Programmlauf -, wird die Ziehungsreihenfolge exakt
nachgespielt: erst 2 x 200 Stoerungen, dann die Grundmatrix.
"""
rng = np.random.default_rng(42)
for A in (GUT, SCHLECHT):
fehlerverstaerkung(A, rng)
grundmatrix = rng.uniform(0.5, 2.0, size=(4, 4))
einheiten = np.array([1e7, 1e-3, 1e5, 1e-2])
return grundmatrix * einheiten[:, None]
def messreihe() -> tuple[list[float], list[float], list[float]]:
"""Eine Familie fast paralleler Zeilen: A = [[1, 1], [1, 1 + eps]].
Mit fallendem eps waechst kappa(A) um zwoelf Groessenordnungen, ohne dass
sich sonst irgendetwas am Modell aendert. Genau das macht die Kurve
aussagekraeftig: Der einzige Unterschied ist die Kondition.
"""
# Eigener Generator, damit die Messreihe den nachgespielten Strom in
# mischmodell() nicht stoert.
rng = np.random.default_rng(7)
kappas, mediane, maxima = [], [], []
for exponent in np.arange(-1.0, -12.5, -0.5):
eps = 10.0 ** exponent
A = np.array([[1.0, 1.0], [1.0, 1.0 + eps]])
median, maximum = fehlerverstaerkung(A, rng)
kappas.append(float(np.linalg.cond(A)))
mediane.append(median)
maxima.append(maximum)
return kappas, mediane, maxima
# --- Zeichnen ---------------------------------------------------------------
def zeichne(kappas, mediane, maxima, kappa_vorher, kappa_nachher) -> None:
figur, (links, rechts) = plt.subplots(
1, 2, figsize=(10.4, 4.3), gridspec_kw={"width_ratios": [1.75, 1]})
# --- links: Verstaerkung gegen kappa ---
# Die Gerade reicht bewusst ueber die Messpunkte hinaus: Innerhalb des
# Messbereichs liegt sie exakt unter der Messkurve - und genau das ist die
# Aussage. Sichtbar bleibt sie dadurch an den Enden.
grenze = np.array([min(kappas) / 30, max(kappas) * 30])
links.plot(grenze, grenze, color="#94a3b8", linewidth=3.0, linestyle="-",
alpha=0.55, zorder=1,
label="theoretische Schranke $y=\\kappa(A)$")
links.plot(kappas, maxima, "o-", color=INDIGO, markersize=4.5, linewidth=1.4,
zorder=3, label="gemessen: größte Verstärkung")
links.plot(kappas, mediane, "s-", color=CYAN, markersize=4.0, linewidth=1.4,
zorder=2, label="gemessen: Median")
marken_rng = np.random.default_rng(7)
for A, name, versatz in ((GUT, "gut konditioniert", (14, -4)),
(SCHLECHT, "fast parallele Zeilen", (-12, 14))):
_, maximum = fehlerverstaerkung(A, marken_rng)
k = float(np.linalg.cond(A))
links.scatter([k], [maximum], s=95, facecolors="none", edgecolors=AMBER,
linewidths=2.0, zorder=4)
links.annotate(name, (k, maximum), textcoords="offset points",
xytext=versatz, fontsize=8, color=AMBER,
ha="right" if versatz[0] < 0 else "left")
links.set_xscale("log")
links.set_yscale("log")
links.set_xlabel("Konditionszahl $\\kappa(A)$")
links.set_ylabel("Fehlerverstärkung (Faktor)")
links.set_title("$\\kappa(A)$ ist eine scharfe Schranke, keine Faustregel",
fontsize=10.5)
links.grid(which="both", linestyle=":", alpha=0.45)
links.legend(fontsize=8, loc="upper left", frameon=False)
links.annotate("Die Messung liegt auf der Schranke:\n"
"$\\kappa$ wird im schlimmsten Fall\nvollständig ausgeschöpft.",
xy=(0.97, 0.06), xycoords="axes fraction", fontsize=8,
ha="right", va="bottom", color="#475569")
for rand in ("top", "right"):
links.spines[rand].set_visible(False)
# --- rechts: was Ruiz ausrichtet ---
balken = rechts.bar(["vorher", "nach Ruiz"], [kappa_vorher, kappa_nachher],
color=[AMBER, GRUEN], width=0.55)
rechts.set_yscale("log")
rechts.set_ylim(1, kappa_vorher * 40)
for stab, wert in zip(balken, (kappa_vorher, kappa_nachher)):
rechts.annotate(f"{wert:.2e}", (stab.get_x() + stab.get_width() / 2, wert),
textcoords="offset points", xytext=(0, 6),
ha="center", fontsize=9, fontweight="bold")
stellen = int(round(np.log10(kappa_vorher / kappa_nachher)))
rechts.set_title(f"Ruiz-Equilibrierung:\n{stellen} Größenordnungen besser",
fontsize=10.5)
rechts.set_ylabel("$\\kappa(A)$")
rechts.grid(axis="y", which="both", linestyle=":", alpha=0.45)
for rand in ("top", "right"):
rechts.spines[rand].set_visible(False)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap_fundament_kondition.{endung}")
figur.savefig(pfad, format=endung, dpi=160,
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)
if __name__ == "__main__":
kappas, mediane, maxima = messreihe()
modell = mischmodell()
kappa_vorher = float(np.linalg.cond(modell))
kappa_nachher = float(np.linalg.cond(ruiz_equilibrierung(modell)))
print(f"Messreihe: {len(kappas)} Matrizen, kappa von {min(kappas):.2e} "
f"bis {max(kappas):.2e}")
print(f"Ruiz: kappa {kappa_vorher:.2e} -> {kappa_nachher:.2e}")
zeichne(kappas, mediane, maxima, kappa_vorher, kappa_nachher)

View file

@ -0,0 +1,299 @@
#!/usr/bin/env python3
# erzeuge_metaheuristik_landschaft.py
"""
Erzeugt das Suchverlauf-Diagramm zum Kapitel Metaheuristiken:
bilder_04/kap_metaheuristiken_landschaft.svg statisch, fuer das PDF
bilder_04/kap_metaheuristiken_landschaft.png Rasterfassung
bilder_04/plotly/kap_metaheuristiken_verlauf.html interaktiv, fuer die Website
Eine Suchlandschaft laesst sich bei 200 Auftraegen nicht zeichnen - der Raum
hat 200! Punkte. Zeichnen laesst sich aber, wie die Suche sie DURCHLAEUFT, und
das zeigt dasselbe:
Links - der Zielwert waehrend der Suche. Bergsteigen faellt monoton und
bleibt liegen. Annealing bei T=1 faellt fast genauso, mit winzigen
Ausschlaegen nach oben. Bei T=8 irrt die Suche weit ueber den
Startwert hinaus und findet nicht zurueck - die Temperatur ist kein
Beiwerk.
Rechts - das Ergebnis ueber die Starttemperatur. Es gibt ein Optimum, es ist
schmal, und jenseits davon wird die Metaheuristik schlechter als die
Faustregel, mit der sie gestartet ist.
Instanz, Zug und Abkuehlplan sind wortgleich zu Simulated_Annealing.py, und
gerechnet wird hier erneut - so koennen Diagramm und Buchtext nicht
auseinanderlaufen. Der einzige Unterschied: Die Suche zeichnet ihren Verlauf
mit, was das Buchprogramm nicht braucht.
Aufruf (aus dem Repository-Wurzelverzeichnis, dauert rund eine Minute):
python3 bilder_04/erzeuge_metaheuristik_landschaft.py
Die interaktive Fassung traegt im Hover die Groesse, die das statische Bild
nicht zeigen kann: die Temperatur an dieser Stelle des Abkuehlplans - und was
sie konkret bedeutet, naemlich mit welcher Wahrscheinlichkeit dort ein Zug
typischer Groesse (Median 70 Minuten, im Kapitel gemessen) angenommen wuerde.
Damit wird an der Kurve ablesbar, warum T0 = 8 so lange braucht, um
zurueckzufinden.
Benoetigt: numpy, matplotlib, plotly
"""
from __future__ import annotations
import math
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import plotly.graph_objects as go
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
PLOTLY_VERZ = os.path.join(BILDER, "plotly")
INDIGO, CYAN, AMBER, GRAU = "#4338ca", "#0891b2", "#b45309", "#94a3b8"
N_AUFTRAEGE = 200
ZUEGE = 400_000
SAAT = 11
STUETZSTELLEN = 800 # so viele Punkte je Verlaufskurve
MEDIAN_ZUG = 70 # typische Zuggroesse in Minuten (im Kapitel gemessen)
# --- Instanz und Zug: wortgleich zu Simulated_Annealing.py ------------------
def erzeuge_ruestmatrix(n: int, saat: int = SAAT) -> np.ndarray:
rng = np.random.default_rng(saat)
familien = max(6, n // 10)
zwischen = rng.integers(8, 60, (familien, familien))
np.fill_diagonal(zwischen, 2)
familie = rng.integers(0, familien, n)
farbe = rng.integers(0, 10, n)
matrix = np.zeros((n, n), dtype=np.int64)
for i in range(n):
for j in range(n):
if i != j:
matrix[i, j] = (zwischen[familie[i], familie[j]]
+ max(0, farbe[i] - farbe[j]) * 3)
return matrix
def gesamtruestzeit(reihe: list[int], matrix: np.ndarray) -> int:
return int(sum(matrix[reihe[k], reihe[k + 1]] for k in range(len(reihe) - 1)))
def faustregel(matrix: np.ndarray) -> list[int]:
n = len(matrix)
offen = set(range(1, n))
reihe = [0]
while offen:
naechster = min(offen, key=lambda j: matrix[reihe[-1], j])
reihe.append(naechster)
offen.discard(naechster)
return reihe
def delta_verschieben(reihe: list[int], matrix: np.ndarray, von: int, nach: int):
n = len(reihe)
if von == nach or nach == von + 1:
return None
heraus = 0
if von > 0:
heraus += matrix[reihe[von - 1], reihe[von]]
if von < n - 1:
heraus += matrix[reihe[von], reihe[von + 1]]
if 0 < von < n - 1:
heraus -= matrix[reihe[von - 1], reihe[von + 1]]
rest = reihe[:von] + reihe[von + 1:]
stelle = nach if nach < von else nach - 1
hinein = 0
if stelle > 0:
hinein += matrix[rest[stelle - 1], reihe[von]]
if stelle < len(rest):
hinein += matrix[reihe[von], rest[stelle]]
if 0 < stelle < len(rest):
hinein -= matrix[rest[stelle - 1], rest[stelle]]
return int(hinein - heraus), rest, stelle
# --- Die Suche, um die Verlaufsaufzeichnung ergaenzt ------------------------
def suche_mit_verlauf(matrix: np.ndarray, start_temperatur: float,
saat: int = 1, bergsteigen: bool = False,
end_temperatur: float = 0.05):
"""Wie suche() im Buchprogramm, protokolliert zusaetzlich den Zielwert.
Aufgezeichnet wird der AKTUELLE Zustand, nicht der beste. Genau darin liegt
die Aussage: Der beste Wert faellt bei jedem Verfahren monoton - was die
Verfahren unterscheidet, ist, wie weit sich die Suche unterwegs vom Guten
entfernen darf.
"""
rng = np.random.default_rng(saat)
n = len(matrix)
reihe = faustregel(matrix)
kosten = gesamtruestzeit(reihe, matrix)
beste_kosten = kosten
takt = max(1, ZUEGE // STUETZSTELLEN)
x, y, temperaturen = [0], [kosten], [start_temperatur]
for zug in range(ZUEGE):
temperatur = start_temperatur * (end_temperatur / start_temperatur) \
** (zug / ZUEGE)
von = int(rng.integers(0, n))
nach = int(rng.integers(0, n + 1))
ergebnis = delta_verschieben(reihe, matrix, von, nach)
if ergebnis is not None:
aenderung, rest, stelle = ergebnis
if aenderung <= 0:
annehmen = True
else:
annehmen = (not bergsteigen
and rng.random() < math.exp(-aenderung / temperatur))
if annehmen:
reihe = rest[:stelle] + [reihe[von]] + rest[stelle:]
kosten += aenderung
beste_kosten = min(beste_kosten, kosten)
if (zug + 1) % takt == 0:
x.append(zug + 1)
y.append(kosten)
temperaturen.append(temperatur)
return x, y, beste_kosten, temperaturen
# --- Zeichnen ---------------------------------------------------------------
def zeichne(verlaeufe, start, temperaturreihe) -> None:
figur, (links, rechts) = plt.subplots(
1, 2, figsize=(10.6, 4.4), gridspec_kw={"width_ratios": [1.7, 1]})
links.axhline(start, color=GRAU, linewidth=1.2, linestyle="--", zorder=1)
links.annotate(f"Faustregel: {start:,}".replace(",", "."),
(ZUEGE, start), textcoords="offset points", xytext=(-4, 6),
ha="right", fontsize=8, color="#475569")
for (name, farbe, breite), (x, y, bestes, _) in verlaeufe:
links.plot(x, y, color=farbe, linewidth=breite, alpha=0.9,
label=f"{name}{bestes:,}".replace(",", "."))
links.set_xlabel("geprüfte Züge")
links.set_ylabel("Rüstzeit des aktuellen Plans (Minuten)")
links.set_title("Wie die Suche die Landschaft durchläuft", fontsize=10.5)
links.grid(linestyle=":", alpha=0.45)
links.legend(fontsize=8, loc="upper right", frameon=False)
links.set_xlim(0, ZUEGE)
for rand in ("top", "right"):
links.spines[rand].set_visible(False)
temperaturen = [t for t, _ in temperaturreihe]
ergebnisse = [e for _, e in temperaturreihe]
rechts.axhline(start, color=GRAU, linewidth=1.2, linestyle="--", zorder=1)
rechts.plot(temperaturen, ergebnisse, "o-", color=INDIGO, markersize=5,
linewidth=1.5, zorder=3)
bester = int(np.argmin(ergebnisse))
rechts.scatter([temperaturen[bester]], [ergebnisse[bester]], s=140,
facecolors="none", edgecolors=AMBER, linewidths=2.2, zorder=4)
rechts.annotate(f"bestes T = {temperaturen[bester]:g}",
(temperaturen[bester], ergebnisse[bester]),
textcoords="offset points", xytext=(10, -2),
fontsize=8, color=AMBER)
rechts.annotate(f"Faustregel: {start:,}".replace(",", "."),
(temperaturen[0], start), textcoords="offset points",
xytext=(2, 4), fontsize=8, color="#475569")
rechts.annotate("hier ist der ganze\nVorsprung aufgezehrt",
(temperaturen[-1], ergebnisse[-1]),
textcoords="offset points", xytext=(-8, -4),
ha="right", va="top", fontsize=8, color="#475569")
rechts.set_ylim(min(ergebnisse) - 8, start + 14)
rechts.set_xscale("log")
rechts.set_xticks(temperaturen)
rechts.set_xticklabels([f"{t:g}" for t in temperaturen])
rechts.set_xlabel("Starttemperatur $T_0$")
rechts.set_ylabel("erreichte Rüstzeit (Minuten)")
rechts.set_title("Das Optimum ist schmal", fontsize=10.5)
rechts.grid(linestyle=":", alpha=0.45)
for rand in ("top", "right"):
rechts.spines[rand].set_visible(False)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap_metaheuristiken_landschaft.{endung}")
figur.savefig(pfad, format=endung, dpi=160,
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)
def zeichne_interaktiv(verlaeufe, start) -> None:
"""Dieselben Verlaeufe, aber mit der Temperatur im Hover.
Das ist der Grund fuer die interaktive Fassung: Im statischen Bild sieht
man, DASS die T0=8-Kurve lange oben bleibt. Hier laesst sich an jeder
Stelle ablesen, WARUM - naemlich wie wahrscheinlich dort ein Zug typischer
Groesse noch angenommen wird.
"""
figur = go.Figure()
figur.add_hline(y=start, line=dict(color=GRAU, width=1.2, dash="dash"),
annotation_text=f"Faustregel: {start}",
annotation_position="bottom right")
for (name, farbe, breite), (x, y, bestes, temps) in verlaeufe:
beschriftung = []
for zug, wert, temperatur in zip(x, y, temps):
quote = math.exp(-MEDIAN_ZUG / temperatur) if temperatur > 0 else 0.0
beschriftung.append(
f"<b>{wert} Minuten</b><br>nach {zug:,} Zügen<br>"
f"Temperatur {temperatur:.3f}<br>"
f"typischer Verschlechterungszug ({MEDIAN_ZUG} min) würde<br>"
f"mit {quote:.1e} angenommen<extra></extra>".replace(",", "."))
figur.add_trace(go.Scatter(
x=x, y=y, mode="lines", name=f"{name}{bestes}",
line=dict(color=farbe, width=breite + 0.4),
hovertemplate=beschriftung))
figur.update_layout(
title="Der Suchverlauf — mit der Temperatur im Hover",
xaxis=dict(title="geprüfte Züge"),
yaxis=dict(title="Rüstzeit des aktuellen Plans (Minuten)"),
height=430, margin=dict(l=70, r=20, t=55, b=50),
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
legend=dict(orientation="h", y=-0.2),
hovermode="closest")
os.makedirs(PLOTLY_VERZ, exist_ok=True)
pfad = os.path.join(PLOTLY_VERZ, "kap_metaheuristiken_verlauf.html")
with open(pfad, "w", encoding="utf-8") as datei:
datei.write(figur.to_html(full_html=False, include_plotlyjs=False,
div_id="kap-metaheuristiken-verlauf"))
print(f"geschrieben: {pfad}")
if __name__ == "__main__":
matrix = erzeuge_ruestmatrix(N_AUFTRAEGE)
start = gesamtruestzeit(faustregel(matrix), matrix)
print(f"Instanz: {N_AUFTRAEGE} Auftraege, Faustregel {start} Minuten")
verlaeufe = []
for beschriftung, kalt, temperatur in (
(("Bergsteigen", CYAN, 1.3), True, 1.0),
(("Annealing $T_0=1$", INDIGO, 1.3), False, 1.0),
(("Annealing $T_0=8$", AMBER, 1.0), False, 8.0)):
x, y, bestes, temps = suche_mit_verlauf(matrix, temperatur, bergsteigen=kalt)
print(f" {beschriftung[0]:<22} bestes {bestes}")
verlaeufe.append((beschriftung, (x, y, bestes, temps)))
temperaturreihe = []
for temperatur in (0.5, 1.0, 2.0, 4.0, 8.0):
_, _, bestes, _ = suche_mit_verlauf(matrix, temperatur)
temperaturreihe.append((temperatur, bestes))
print(f" T0={temperatur:<5} -> {bestes}")
zeichne(verlaeufe, start, temperaturreihe)
zeichne_interaktiv(verlaeufe, start)

View file

@ -0,0 +1,148 @@
#!/usr/bin/env python3
# erzeuge_min_cost_flow.py
"""
Erzeugt das Flussdiagramm zum Graphen-Kapitel:
bilder_04/kap_graphen_min_cost_flow.svg / .png
Das Vorgaengerbild zeigte die Instanz von Min_Cost_Flow.py korrekt - aber nur
die Instanz. Kosten und Kapazitaeten standen an den Kanten, der FLUSS nicht.
Damit fehlte genau das, was das Kapitel erklaert.
Hier wird das Netzwerk geloest und das Ergebnis eingezeichnet:
* Kantenbeschriftung "Fluss / Kapazitaet, Kosten je Einheit"
* Strichstaerke waechst mit dem Fluss
* ausgelastete Kanten hervorgehoben, ungenutzte gestrichelt und blass
* Knotenpreise (die Dualwerte der Flusserhaltung) an den Knoten
Zwei Dinge werden dadurch sichtbar, die im alten Bild fehlten: Die billige
Route ueber den Umschlag laeuft voll, und trotzdem muss die teure Direktkante
Werk_B -> Kunde_2 bedient werden - weil der Umschlagweg dorthin schon
ausgelastet ist. Genau das ist der Punkt des Abschnitts.
Modell und Loesungsweg sind wortgleich zu Min_Cost_Flow.py, und gerechnet wird
hier erneut. Das Skript prueft, dass Zielwert und Fluesse zum Programmlauf
passen - stimmt etwas nicht, bricht es ab.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_min_cost_flow.py
Benoetigt: numpy, scipy, graphviz (Python-Paket) und das Werkzeug 'dot'
"""
from __future__ import annotations
import os
import graphviz
import numpy as np
from scipy.optimize import linprog
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, CYAN, VIOLETT, GRUEN, AMBER = (
"#4338ca", "#0891b2", "#7c3aed", "#059669", "#b45309")
TINT = {INDIGO: "#eef2ff", CYAN: "#ecfeff", VIOLETT: "#f5f3ff",
GRUEN: "#ecfdf5", AMBER: "#fffbeb"}
BLASS = "#cbd5e1"
# --- Netzwerk: wortgleich zu Min_Cost_Flow.py -------------------------------
KNOTEN = ["Werk_A", "Werk_B", "Umschlag", "Kunde_1", "Kunde_2"]
KANTEN = [
("Werk_A", "Umschlag", 2.0, 15),
("Werk_A", "Kunde_1", 5.0, 10),
("Werk_B", "Umschlag", 4.0, 10),
("Werk_B", "Kunde_2", 6.0, 10),
("Umschlag", "Kunde_1", 1.0, 20),
("Umschlag", "Kunde_2", 3.0, 10),
]
SALDO = {"Werk_A": 20, "Werk_B": 10, "Umschlag": 0, "Kunde_1": -15, "Kunde_2": -15}
ERWARTETE_KOSTEN = 140.0 # aus dem abgedruckten Programmlauf
def loese():
"""Flussproblem als LP - derselbe Aufbau wie im Buchprogramm."""
knoten_index = {k: i for i, k in enumerate(KNOTEN)}
kosten = np.array([k[2] for k in KANTEN])
A_eq = np.zeros((len(KNOTEN), len(KANTEN)))
for e, (von, nach, _, _) in enumerate(KANTEN):
A_eq[knoten_index[von], e] = +1.0
A_eq[knoten_index[nach], e] = -1.0
b_eq = np.array([SALDO[k] for k in KNOTEN], dtype=float)
schranken = [(0, k[3]) for k in KANTEN]
ergebnis = linprog(c=kosten, A_eq=A_eq, b_eq=b_eq, bounds=schranken,
method="highs")
if not ergebnis.success:
raise SystemExit(f"Nicht loesbar: {ergebnis.message}")
return float(ergebnis.fun), np.asarray(ergebnis.x), np.asarray(ergebnis.eqlin.marginals)
def zahl(wert: float) -> str:
text = f"{wert:.2f}".rstrip("0").rstrip(".")
if text in ("-0", "-"):
text = "0"
return text.replace(".", ",")
def zeichne(gesamtkosten: float, fluss: np.ndarray, preise: np.ndarray) -> None:
punkt = graphviz.Digraph(format="svg")
punkt.attr(rankdir="LR", splines="spline", nodesep="0.5", ranksep="1.1",
fontname="DejaVu Sans", bgcolor="transparent", fontsize="12",
labelloc="t",
label=(f"Kostenminimaler Fluss: {zahl(gesamtkosten)} € für 30 Einheiten\\n"
"Kantenbeschriftung: Fluss / Kapazität, Kosten je Einheit"))
punkt.attr("node", fontname="DejaVu Sans", fontsize="10")
punkt.attr("edge", fontname="DejaVu Sans", fontsize="9")
preis_von = dict(zip(KNOTEN, preise))
for name in KNOTEN:
saldo = SALDO[name]
if saldo > 0:
farbe, rolle = GRUEN, f"Angebot +{saldo}"
elif saldo < 0:
farbe, rolle = INDIGO, f"Bedarf {saldo}"
else:
farbe, rolle = VIOLETT, "Durchleitung"
punkt.node(name, f"{name.replace('_', ' ')}\\n{rolle}\\n"
f"Knotenpreis {zahl(preis_von[name])}",
shape="box", style="filled,rounded", fillcolor=TINT[farbe],
color=farbe, penwidth="2.0", margin="0.16,0.09")
for (von, nach, kosten, kapazitaet), menge in zip(KANTEN, fluss):
voll = menge > kapazitaet - 1e-7
leer = menge < 1e-7
beschriftung = (f"{zahl(menge)} / {kapazitaet}, {zahl(kosten)}"
+ (" ⟵ voll" if voll else ""))
if leer:
punkt.edge(von, nach, label=f"{beschriftung} (ungenutzt)",
color=BLASS, fontcolor="#94a3b8", style="dashed",
penwidth="1.0")
else:
punkt.edge(von, nach, label=beschriftung,
color=AMBER if voll else CYAN,
fontcolor=AMBER if voll else "#334155",
penwidth=f"{1.0 + 3.0 * menge / kapazitaet:.1f}")
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
punkt.format = endung
pfad = punkt.render(os.path.join(BILDER, "kap_graphen_min_cost_flow"),
cleanup=True)
print(f"geschrieben: {pfad}")
if __name__ == "__main__":
gesamtkosten, fluss, preise = loese()
print(f"Minimale Transportkosten: {gesamtkosten:.2f} EUR")
for (von, nach, kosten, kapazitaet), menge in zip(KANTEN, fluss):
marke = " (VOLL)" if menge > kapazitaet - 1e-7 else ""
print(f" {von:9} -> {nach:9} {menge:6.1f} / {kapazitaet:3}{marke}")
if abs(gesamtkosten - ERWARTETE_KOSTEN) > 1e-6:
raise SystemExit(f"FEHLER: Zielwert {gesamtkosten} weicht vom "
f"abgedruckten Programmlauf ({ERWARTETE_KOSTEN}) ab.")
zeichne(gesamtkosten, fluss, preise)

View file

@ -0,0 +1,208 @@
#!/usr/bin/env python3
# erzeuge_pareto_front.py
"""
Erzeugt das Pareto-Front-Diagramm zum Mehrziel-Kapitel in zwei Fassungen:
bilder_04/kap_mehrziel_pareto.svg statisch, fuer das PDF
bilder_04/kap_mehrziel_pareto.png statisch, Rasterfassung
bilder_04/plotly/kap_mehrziel_pareto.html interaktiv, fuer die Website
Die Instanz ist identisch mit Mehrziel_Pareto.py, und gerechnet wird hier
erneut - so koennen Diagramm und Buchtext nicht auseinanderlaufen.
Das Diagramm zeigt genau die Aussage des Kapitels: Die untere konvexe Huelle
(gestrichelt) ist alles, was eine gewichtete Summe je erreichen kann. Die
Punkte darueber sind Pareto-optimal und trotzdem fuer kein Gewicht zu haben.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_pareto_front.py
Benoetigt: numpy, scipy, matplotlib, plotly
"""
from __future__ import annotations
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import plotly.graph_objects as go
from scipy.optimize import linprog
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
PLOTLY_VERZ = os.path.join(BILDER, "plotly")
INDIGO, CYAN, AMBER = "#4338ca", "#0891b2", "#b45309"
BAHN_TRASSEN = 5
# --- Instanz und Modell: wortgleich zu Mehrziel_Pareto.py -------------------
def erzeuge_sendungen(anzahl: int = 12, saat: int = 5):
rng = np.random.default_rng(saat)
kosten = np.zeros((anzahl, 3))
co2 = np.zeros((anzahl, 3))
for i in range(anzahl):
grund_kosten = rng.integers(600, 2400)
grund_co2 = rng.integers(400, 1800)
kosten[i] = [grund_kosten,
grund_kosten * rng.uniform(0.55, 0.85),
grund_kosten * rng.uniform(0.70, 1.00)]
co2[i] = [grund_co2,
grund_co2 * rng.uniform(0.15, 0.35),
grund_co2 * rng.uniform(0.40, 0.70)]
return np.round(kosten).astype(int), np.round(co2).astype(int)
KOSTEN, CO2 = erzeuge_sendungen()
N = len(KOSTEN)
def plane(ziel, co2_grenze=None):
gleichungen = np.zeros((N, N * 3))
for i in range(N):
gleichungen[i, i * 3:(i + 1) * 3] = 1.0
ungleichungen = [[1.0 if j == 1 else 0.0 for _ in range(N) for j in range(3)]]
grenzen = [float(BAHN_TRASSEN)]
if co2_grenze is not None:
ungleichungen.append(CO2.reshape(-1).astype(float))
grenzen.append(float(co2_grenze))
ergebnis = linprog(ziel.reshape(-1).astype(float),
A_ub=ungleichungen, b_ub=grenzen,
A_eq=gleichungen, b_eq=np.ones(N),
bounds=(0, 1), integrality=1, method="highs")
if not ergebnis.success:
return None
plan = np.round(ergebnis.x).astype(int)
return int(KOSTEN.reshape(-1) @ plan), int(CO2.reshape(-1) @ plan)
def pareto_front():
front = []
grenze = plane(KOSTEN)[1]
while True:
ergebnis = plane(KOSTEN, co2_grenze=grenze)
if ergebnis is None:
return front
front.append(ergebnis)
grenze = ergebnis[1] - 1
def untere_huelle(punkte):
huelle = []
for punkt in sorted(punkte):
while len(huelle) >= 2:
(x1, y1), (x2, y2) = huelle[-2], huelle[-1]
if (x2 - x1) * (punkt[1] - y1) - (y2 - y1) * (punkt[0] - x1) <= 0:
huelle.pop()
else:
break
huelle.append(punkt)
return huelle
# --- Statische Fassung ------------------------------------------------------
def zeichne_statisch(front, huelle) -> None:
unerreichbar = [p for p in front if p not in huelle]
figur, achse = plt.subplots(figsize=(7.2, 4.4))
hx, hy = zip(*huelle)
achse.plot(hx, hy, linestyle="--", color=INDIGO, linewidth=1.4, zorder=1,
label="untere konvexe Hülle")
achse.scatter(hx, hy, s=70, color=INDIGO, zorder=3,
label=f"von Gewichten erreichbar ({len(huelle)})")
if unerreichbar:
ux, uy = zip(*unerreichbar)
achse.scatter(ux, uy, s=90, facecolors="none", edgecolors=AMBER,
linewidths=2.0, zorder=4,
label=f"Pareto-optimal, aber unerreichbar ({len(unerreichbar)})")
for x, y in front:
achse.annotate(f"{y:,} kg".replace(",", "."), (x, y),
textcoords="offset points", xytext=(6, 6), fontsize=7,
color="#475569")
achse.set_xlabel("Transportkosten (EUR)")
achse.set_ylabel("CO₂-Ausstoß (kg)")
achse.set_title("Pareto-Front: Kosten gegen CO₂", fontsize=11)
achse.grid(linestyle=":", alpha=0.5)
for rand in ("top", "right"):
achse.spines[rand].set_visible(False)
achse.legend(fontsize=8, loc="upper right", frameon=False)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap_mehrziel_pareto.{endung}")
figur.savefig(pfad, format=endung, dpi=160,
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)
# --- Interaktive Fassung ----------------------------------------------------
def zeichne_interaktiv(front, huelle) -> None:
unerreichbar = [p for p in front if p not in huelle]
guenstigst = front[0]
figur = go.Figure()
hx, hy = zip(*huelle)
figur.add_trace(go.Scatter(
x=hx, y=hy, mode="lines", line=dict(color=INDIGO, dash="dash", width=1.5),
name="untere konvexe Hülle", hoverinfo="skip"))
def beschriftung(punkt):
aufpreis = punkt[0] - guenstigst[0]
ersparnis = guenstigst[1] - punkt[1]
preis = f"{aufpreis / ersparnis:.2f} EUR je kg" if ersparnis else "Kostenminimum"
return (f"<b>{punkt[0]:,} EUR</b><br>{punkt[1]:,} kg CO₂<br>"
f"Aufpreis {aufpreis:,} EUR<br>"
f"Ersparnis {ersparnis:,} kg<br>{preis}<extra></extra>")
figur.add_trace(go.Scatter(
x=hx, y=hy, mode="markers", marker=dict(size=11, color=INDIGO),
name=f"von Gewichten erreichbar ({len(huelle)})",
hovertemplate=[beschriftung(p) for p in huelle]))
if unerreichbar:
ux, uy = zip(*unerreichbar)
figur.add_trace(go.Scatter(
x=ux, y=uy, mode="markers",
marker=dict(size=13, color="rgba(0,0,0,0)",
line=dict(color=AMBER, width=2.5)),
name=f"Pareto-optimal, aber unerreichbar ({len(unerreichbar)})",
hovertemplate=[beschriftung(p) for p in unerreichbar]))
figur.update_layout(
title="Pareto-Front: Kosten gegen CO₂",
xaxis=dict(title="Transportkosten (EUR)"),
yaxis=dict(title="CO₂-Ausstoß (kg)"),
height=430, margin=dict(l=70, r=20, t=55, b=50),
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
legend=dict(orientation="h", y=-0.2),
hovermode="closest")
os.makedirs(PLOTLY_VERZ, exist_ok=True)
pfad = os.path.join(PLOTLY_VERZ, "kap_mehrziel_pareto.html")
with open(pfad, "w", encoding="utf-8") as datei:
datei.write(figur.to_html(full_html=False, include_plotlyjs=False,
div_id="kap-mehrziel-pareto"))
print(f"geschrieben: {pfad}")
if __name__ == "__main__":
front = pareto_front()
huelle = untere_huelle(front)
print(f"Pareto-Punkte: {len(front)}, davon gestuetzt: {len(huelle)}, "
f"unerreichbar: {len(front) - len(huelle)}")
zeichne_statisch(front, huelle)
zeichne_interaktiv(front, huelle)

View file

@ -0,0 +1,254 @@
#!/usr/bin/env python3
# erzeuge_polyeder.py
"""
Erzeugt die beiden Polyeder-Diagramme zum Fundament- und zum LP-Kapitel:
bilder_04/kap_fundament_polyeder.svg / .png der zulaessige Bereich
bilder_04/kap_lp_simplex_ecken.svg / .png der Eckenlauf des Simplex
Beide zeigen DASSELBE Modell - das Bot-Allokationsproblem, das sich durch die
ersten Kapitel zieht:
max 150 x1 + 250 x2
u.d.N. 2 x1 + 5 x2 <= 40 (vCPU)
4 x1 + 6 x2 <= 60 (RAM)
x1 <= 8 (Marktlimit)
x1, x2 >= 0
Warum das wichtig ist: Die Vorgaengerbilder zeigten ein voellig anderes LP
(2x1+3x2 <= 24, 3x1+2x2 <= 24, Optimum bei (4,8; 4,8)) - unmittelbar neben einer
Handrechnung, die mit dem Bot-Modell rechnet und (7,5; 5) herausbekommt. Ein
Leser musste annehmen, beides gehoere zusammen.
Die Ecken werden NICHT hingeschrieben, sondern nach demselben Verfahren
bestimmt wie in Visualisierung_Loesungsraum.py: Schnittpunkte je zweier
Begrenzungsgeraden, die zulaessig sind. Der Simplex-Weg im zweiten Bild ist der
Weg der Handrechnung im LP-Kapitel: (0,0) -> (0,8) -> (7,5; 5), also drei der
fuenf Ecken.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_polyeder.py
Benoetigt: numpy, matplotlib
"""
from __future__ import annotations
import itertools
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, CYAN, AMBER, GRUEN = "#4338ca", "#0891b2", "#b45309", "#059669"
# Modell wortgleich zu Visualisierung_Loesungsraum.py
C = np.array([150.0, 250.0])
A = np.array([[2.0, 5.0], [4.0, 6.0], [1.0, 0.0]])
B = np.array([40.0, 60.0, 8.0])
NAMEN = ["vCPU: $2x_1+5x_2 \\leq 40$", "RAM: $4x_1+6x_2 \\leq 60$",
"Marktlimit: $x_1 \\leq 8$"]
FARBEN = [INDIGO, CYAN, AMBER]
# Der Weg der Handrechnung im LP-Kapitel: Start im Ursprung, dann x2 aufnehmen
# (Pivotspalte -250), dann x1 (Pivotspalte -50).
SIMPLEXWEG = [(0.0, 0.0), (0.0, 8.0), (7.5, 5.0)]
def ist_zulaessig(punkt: np.ndarray, tol: float = 1e-7) -> bool:
return bool(np.all(A @ punkt <= B + tol) and np.all(punkt >= -tol))
def berechne_ecken() -> np.ndarray:
"""Ecken = zulaessige Schnittpunkte je zweier Begrenzungsgeraden.
Wortgleich zu Visualisierung_Loesungsraum.py - die Koordinaten im Bild
stammen also aus derselben Rechnung wie die im Buch abgedruckte Tabelle,
nicht aus einer zweiten Quelle.
"""
geraden = [(A[i], B[i]) for i in range(len(B))]
geraden.append((np.array([1.0, 0.0]), 0.0)) # x1 = 0
geraden.append((np.array([0.0, 1.0]), 0.0)) # x2 = 0
ecken: list[np.ndarray] = []
for (n1, d1), (n2, d2) in itertools.combinations(geraden, 2):
matrix = np.array([n1, n2])
if abs(np.linalg.det(matrix)) < 1e-9:
continue
punkt = np.linalg.solve(matrix, np.array([d1, d2]))
if ist_zulaessig(punkt) and not any(np.allclose(punkt, e) for e in ecken):
ecken.append(punkt)
return np.array(ecken)
# Beschriftungsposition je Ecke, in Punkten relativ zum Eckpunkt. Von Hand
# gesetzt und nicht gerechnet: (7,5; 5) und (8; 4,67) liegen eine halbe Einheit
# auseinander, und jede automatische Regel legt ihre Beschriftungen uebereinander.
# Das ist Layout, keine Aussage - die Koordinaten selbst bleiben gerechnet.
VERSATZ = {
(0.0, 0.0): (14, 10),
(0.0, 8.0): (14, -2),
(8.0, 0.0): (-10, 14),
(7.5, 5.0): (-12, 16),
(8.0, 4.67): (16, -4),
}
# Im Simplex-Bild laeuft der Pfad durch zwei dieser Positionen - dort andere.
VERSATZ_SIMPLEX = {
(0.0, 8.0): (16, 12),
(7.5, 5.0): (14, 16),
}
def versatz(punkt: np.ndarray, mit_pfad: bool = False) -> tuple:
schluessel = (round(float(punkt[0]), 2), round(float(punkt[1]), 2))
if mit_pfad and schluessel in VERSATZ_SIMPLEX:
return VERSATZ_SIMPLEX[schluessel]
return VERSATZ.get(schluessel, (12, 8))
def punkt_text(punkt: np.ndarray) -> str:
"""Koordinaten deutsch und auf zwei Stellen - '(8; 4,67)' statt '(8; 4.66667)'."""
teile = []
for wert in punkt:
text = f"{wert:.2f}".rstrip("0").rstrip(".")
teile.append(text.replace(".", ","))
return f"({teile[0]}; {teile[1]})"
def euro(wert: float) -> str:
return f"{wert:,.0f}".replace(",", ".")
def sortiere_umlaufend(ecken: np.ndarray) -> np.ndarray:
"""Ecken gegen den Uhrzeigersinn ordnen, damit das Polygon nicht verknotet."""
mitte = ecken.mean(axis=0)
winkel = np.arctan2(ecken[:, 1] - mitte[1], ecken[:, 0] - mitte[0])
return ecken[np.argsort(winkel)]
def zeichne_rahmen(achse, ecken: np.ndarray) -> None:
"""Restriktionsgeraden, zulaessiger Bereich und Achsenbeschriftung."""
rand = sortiere_umlaufend(ecken)
achse.fill(rand[:, 0], rand[:, 1], color=INDIGO, alpha=0.10, zorder=1)
achse.plot(np.append(rand[:, 0], rand[0, 0]),
np.append(rand[:, 1], rand[0, 1]),
color=INDIGO, linewidth=1.6, zorder=2)
x = np.linspace(0, 11, 200)
for zeile, grenze, name, farbe in zip(A, B, NAMEN, FARBEN):
if abs(zeile[1]) < 1e-9: # senkrechte Gerade x1 = c
achse.axvline(grenze / zeile[0], color=farbe, linewidth=1.3,
linestyle="--", alpha=0.8, label=name)
else:
achse.plot(x, (grenze - zeile[0] * x) / zeile[1], color=farbe,
linewidth=1.3, linestyle="--", alpha=0.8, label=name)
achse.set_xlim(-0.9, 11.4)
achse.set_ylim(-0.9, 11)
achse.set_xlabel("$x_1$ — Bots vom Typ A")
achse.set_ylabel("$x_2$ — Bots vom Typ B")
achse.grid(linestyle=":", alpha=0.45)
for seite in ("top", "right"):
achse.spines[seite].set_visible(False)
def schreibe(figur, name: str) -> None:
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"{name}.{endung}")
figur.savefig(pfad, format=endung, dpi=160,
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)
def zeichne_polyeder(ecken: np.ndarray, bestes: np.ndarray) -> None:
"""Fundament: alle Ecken mit ihrem Zielwert - der Fundamentalsatz zum Anfassen."""
figur, achse = plt.subplots(figsize=(7.2, 5.2))
zeichne_rahmen(achse, ecken)
for ecke in ecken:
wert = float(C @ ecke)
optimal = np.allclose(ecke, bestes)
achse.scatter(*ecke, s=120 if optimal else 55, zorder=4,
color=GRUEN if optimal else INDIGO)
beschriftung = f"{punkt_text(ecke)}\n{euro(wert)}"
if optimal:
beschriftung += "\n← Optimum"
weg = versatz(ecke)
achse.annotate(beschriftung, ecke, textcoords="offset points",
xytext=weg, fontsize=8.5,
ha="left" if weg[0] >= 0 else "right",
va="bottom" if weg[1] >= 0 else "top",
color=GRUEN if optimal else "#334155",
fontweight="bold" if optimal else "normal")
achse.set_title("Der zulässige Bereich und seine Ecken\n"
"$\\max\\ 150x_1 + 250x_2$", fontsize=11)
achse.legend(fontsize=8, loc="upper right", frameon=False)
figur.tight_layout()
schreibe(figur, "kap_fundament_polyeder")
def zeichne_simplexweg(ecken: np.ndarray, bestes: np.ndarray) -> None:
"""LP-Kapitel: derselbe Bereich, aber der Weg der Handrechnung."""
figur, achse = plt.subplots(figsize=(7.2, 5.2))
zeichne_rahmen(achse, ecken)
besucht = [np.array(p) for p in SIMPLEXWEG]
unbesucht = [e for e in ecken
if not any(np.allclose(e, p) for p in besucht)]
for ecke in unbesucht:
achse.scatter(*ecke, s=55, color="#94a3b8", zorder=3)
weg = versatz(ecke, mit_pfad=True)
achse.annotate(punkt_text(ecke), ecke, textcoords="offset points",
xytext=weg, fontsize=8, color="#64748b",
ha="left" if weg[0] >= 0 else "right",
va="bottom" if weg[1] >= 0 else "top")
weg = np.array(besucht)
achse.plot(weg[:, 0], weg[:, 1], color=AMBER, linewidth=2.4, zorder=4)
for schritt, punkt in enumerate(besucht):
wert = float(C @ punkt)
letzter = schritt == len(besucht) - 1
achse.scatter(*punkt, s=140 if letzter else 90, zorder=5,
color=GRUEN if letzter else AMBER)
weg = versatz(punkt, mit_pfad=True)
achse.annotate(f"{schritt}. {punkt_text(punkt)}\nZ = {euro(wert)}",
punkt, textcoords="offset points", xytext=weg,
fontsize=8.5, fontweight="bold",
ha="left" if weg[0] >= 0 else "right",
va="bottom" if weg[1] >= 0 else "top",
color=GRUEN if letzter else "#334155")
achse.set_title(f"Der Simplex besucht {len(besucht)} von {len(ecken)} Ecken\n"
"und keine davon zweimal", fontsize=11)
achse.legend(fontsize=8, loc="upper right", frameon=False)
figur.tight_layout()
schreibe(figur, "kap_lp_simplex_ecken")
if __name__ == "__main__":
ecken = berechne_ecken()
werte = ecken @ C
bestes = ecken[int(np.argmax(werte))]
print(f"{len(ecken)} Ecken gefunden:")
for ecke, wert in sorted(zip(ecken.tolist(), werte.tolist()),
key=lambda p: -p[1]):
print(f" ({ecke[0]:6.2f}; {ecke[1]:5.2f}) Z = {wert:9,.2f} EUR")
print(f"Optimum: ({bestes[0]:g}; {bestes[1]:g})")
for punkt in SIMPLEXWEG:
if not any(np.allclose(np.array(punkt), e) for e in ecken):
raise SystemExit(f"FEHLER: {punkt} ist keine Ecke des Polyeders.")
zeichne_polyeder(ecken, bestes)
zeichne_simplexweg(ecken, bestes)

231
bilder_04/erzeuge_runden.py Normal file
View file

@ -0,0 +1,231 @@
#!/usr/bin/env python3
# erzeuge_runden.py
"""
Erzeugt das Diagramm "Warum Runden scheitert" zum MILP-Kapitel:
bilder_04/kap_milp_runden.svg statisch, fuer das PDF
bilder_04/kap_milp_runden.png Rasterfassung
Zwei Bilder nebeneinander, beide aus dem Kapitel selbst:
Links - die Handrechnung des Kapitels, gezeichnet:
max x1 + x2 u.d.N. 2 x1 + 2 x2 <= 3, x ganzzahlig
Der Witz ist die Entartung: Die Zielfunktion laeuft PARALLEL zur
Restriktion, also ist die ganze Kante optimal, nicht eine Ecke.
Aufrunden auf (1,1) fuehrt aus dem zulaessigen Bereich heraus,
Abrunden auf (0,0) auf den Wert null. Beides sieht man sofort.
Rechts - die Messung aus Runden_Gegenbeispiel.py, mit derselben Saat
nachgerechnet: Wie oft ist die aufgerundete Loesung unzulaessig,
und was kostet Abrunden - mit und ohne gieriges Auffuellen?
Warum ueberhaupt neu: Das Vorgaengerbild zeigte ein LP, das im Buch nicht
vorkommt (LP-Optimum bei (1,8; 3,4), Z = 42,6), ohne die Zielfunktion zu nennen
- unmittelbar ueber der Handrechnung, die etwas voellig anderes rechnet.
Aufruf (aus dem Repository-Wurzelverzeichnis, dauert rund eine Minute):
python3 bilder_04/erzeuge_runden.py
Benoetigt: numpy, scipy, matplotlib
"""
from __future__ import annotations
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import linprog
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, CYAN, AMBER, GRUEN, ROT = (
"#4338ca", "#0891b2", "#b45309", "#059669", "#be123c")
GROESSEN = [(5, 2), (10, 3), (20, 5), (40, 8)]
INSTANZEN = 200
SAAT = 2026
# --- Messung: wortgleich zu Runden_Gegenbeispiel.py -------------------------
def erzeuge_instanz(n, m, rng):
c = rng.integers(3, 20, size=n).astype(float)
A = rng.integers(1, 9, size=(m, n)).astype(float)
b = (A.sum(axis=1) * rng.uniform(0.25, 0.45)).round()
return c, A, b
def loese_lp(c, A, b, ganzzahlig=False):
n = len(c)
ergebnis = linprog(
c=-c, A_ub=A, b_ub=b, bounds=[(0, None)] * n,
integrality=np.ones(n) if ganzzahlig else None,
method="highs")
return (-ergebnis.fun, ergebnis.x) if ergebnis.success else (None, None)
def abrunden_und_reparieren(x_lp, c, A, b):
x = np.floor(x_lp + 1e-9)
verbessert = True
while verbessert:
verbessert = False
for j in np.argsort(-c):
kandidat = x.copy()
kandidat[j] += 1
if np.all(A @ kandidat <= b + 1e-9):
x = kandidat
verbessert = True
break
return c @ x, x
def messreihe():
"""Dieselbe Schleife wie im Buchprogramm, mit derselben Saat.
Die Saat sitzt VOR der Schleife ueber die Groessen, nicht darin - so wie
dort. Nur deshalb kommen hier dieselben Zahlen heraus wie in der
abgedruckten Tabelle.
"""
rng = np.random.default_rng(SAAT)
zeilen = []
for n, m in GROESSEN:
unzulaessig = 0
verluste_ab, verluste_gierig = [], []
gezaehlt = 0
for _ in range(INSTANZEN):
c, A, b = erzeuge_instanz(n, m, rng)
z_lp, x_lp = loese_lp(c, A, b, ganzzahlig=False)
z_ip, _ = loese_lp(c, A, b, ganzzahlig=True)
if z_lp is None or z_ip is None or z_ip <= 0:
continue
gezaehlt += 1
x_auf = np.ceil(x_lp - 1e-9)
if np.any(A @ x_auf > b + 1e-9):
unzulaessig += 1
x_ab = np.floor(x_lp + 1e-9)
verluste_ab.append(1.0 - (c @ x_ab) / z_ip)
z_gierig, _ = abrunden_und_reparieren(x_lp, c, A, b)
verluste_gierig.append(1.0 - z_gierig / z_ip)
zeilen.append({
"name": f"{n}×{m}",
"unzulaessig": unzulaessig / gezaehlt,
"ab": float(np.mean(verluste_ab)),
"gierig": float(np.mean(verluste_gierig)),
})
return zeilen
# --- Zeichnen ---------------------------------------------------------------
def zeichne_handrechnung(achse) -> None:
"""Die Handrechnung des Kapitels: max x1+x2 u.d.N. 2x1+2x2 <= 3."""
ecken = np.array([[0.0, 0.0], [1.5, 0.0], [0.0, 1.5]])
achse.fill(ecken[:, 0], ecken[:, 1], color=CYAN, alpha=0.13, zorder=1)
achse.plot([1.5, 0.0], [0.0, 1.5], color=CYAN, linewidth=2.0, zorder=3,
label="$2x_1+2x_2 = 3$ — zugleich das LP-Optimum")
# Ganzzahlige Punkte im Fenster, zulaessig und unzulaessig getrennt
for x1 in range(0, 3):
for x2 in range(0, 3):
zulaessig = 2 * x1 + 2 * x2 <= 3
achse.scatter(x1, x2, s=48, zorder=4,
color=INDIGO if zulaessig else "#cbd5e1")
achse.scatter([0.75], [0.75], marker="*", s=260, color=ROT, zorder=6)
achse.annotate("LP-Lösung $(0{,}75;\\ 0{,}75)$\n$Z_{LP} = 1{,}5$",
(0.75, 0.75), textcoords="offset points", xytext=(-12, -6),
ha="right", va="top", fontsize=9, color=ROT,
fontweight="bold")
achse.scatter([1], [1], s=150, facecolors="none", edgecolors=AMBER,
linewidths=2.4, zorder=6)
achse.annotate("aufgerundet $(1;1)$\n$2+2=4>3$ — unzulässig",
(1, 1), textcoords="offset points", xytext=(12, -6),
fontsize=8.5, color=AMBER, va="top")
achse.scatter([0], [0], s=150, facecolors="none", edgecolors="#64748b",
linewidths=2.2, zorder=6)
achse.annotate("abgerundet $(0;0)$\n$Z = 0$ — alles verschenkt",
(0, 0), textcoords="offset points", xytext=(14, 8),
fontsize=8.5, color="#475569")
for punkt in ((1, 0), (0, 1)):
achse.scatter(*punkt, s=140, color=GRUEN, zorder=7)
achse.annotate("wahres Optimum $Z_{IP}=1$", (1, 0),
textcoords="offset points", xytext=(12, -4),
va="top", fontsize=8.5, color=GRUEN, fontweight="bold")
achse.set_xlim(-0.35, 2.35)
achse.set_ylim(-0.35, 2.35)
achse.set_xlabel("$x_1$")
achse.set_ylabel("$x_2$")
achse.set_title("Die Handrechnung als Bild:\nrunden geht in beide Richtungen schief",
fontsize=10.5)
achse.grid(linestyle=":", alpha=0.45)
achse.legend(fontsize=8, loc="upper right", frameon=False)
for seite in ("top", "right"):
achse.spines[seite].set_visible(False)
def zeichne_messung(achse, zeilen) -> None:
namen = [z["name"] for z in zeilen]
stellen = np.arange(len(zeilen))
breite = 0.26
achse.bar(stellen - breite, [z["unzulaessig"] * 100 for z in zeilen],
breite, color=AMBER, label="Aufrunden unzulässig")
achse.bar(stellen, [z["ab"] * 100 for z in zeilen],
breite, color=ROT, label="Abrunden: mittlerer Verlust")
achse.bar(stellen + breite, [z["gierig"] * 100 for z in zeilen],
breite, color=GRUEN, label="abrunden + gierig auffüllen")
for stelle, zeile in zip(stellen, zeilen):
for versatz, wert in ((-breite, zeile["unzulaessig"]),
(0.0, zeile["ab"]), (breite, zeile["gierig"])):
achse.annotate(f"{wert * 100:.0f}", (stelle + versatz, wert * 100),
textcoords="offset points", xytext=(0, 3),
ha="center", fontsize=7.5, color="#334155")
achse.set_xticks(stellen)
achse.set_xticklabels(namen)
achse.set_xlabel("Instanzgröße (Variablen × Nebenbedingungen)")
achse.set_ylabel("Anteil bzw. Verlust (%)")
achse.set_ylim(0, 118)
achse.set_title(f"Gemessen an je {INSTANZEN} Zufallsinstanzen:\n"
"Aufrunden wird mit der Größe hoffnungslos", fontsize=10.5)
achse.grid(axis="y", linestyle=":", alpha=0.45)
achse.legend(fontsize=8, loc="upper center", frameon=False, ncol=1)
for seite in ("top", "right"):
achse.spines[seite].set_visible(False)
def zeichne(zeilen) -> None:
figur, (links, rechts) = plt.subplots(1, 2, figsize=(11.0, 4.8))
zeichne_handrechnung(links)
zeichne_messung(rechts, zeilen)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap_milp_runden.{endung}")
figur.savefig(pfad, format=endung, dpi=160,
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)
if __name__ == "__main__":
zeilen = messreihe()
print(f"{'n x m':>8} | {'Aufrunden unzul.':>17} | {'Abrunden':>10} | {'gierig':>8}")
print("-" * 54)
for zeile in zeilen:
print(f"{zeile['name']:>8} | {zeile['unzulaessig'] * 100:>16.1f} % | "
f"{zeile['ab'] * 100:>9.1f} % | {zeile['gierig'] * 100:>7.1f} %")
zeichne(zeilen)

View file

@ -0,0 +1,132 @@
#!/usr/bin/env python3
# erzeuge_solverwahl_diagramme.py
"""
Erzeugt die beiden Entscheidungs-Flussdiagramme "Welches Werkzeug passt zu
meinem Problem?" fuer die Auftakte von Teil II und Teil III:
bilder_04/teil2_solverwahl.svg / .png deterministische Verfahren
bilder_04/teil3_solverwahl.svg / .png Nichtlinearitaet und Unsicherheit
Beide entstehen mit Graphviz. Die Farben folgen der Buchpalette
(Indigo, Cyan, Violett, Gruen, Amber), die Kastenform codiert die Rolle:
Raute = Frage, abgerundeter Kasten = Empfehlung.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_solverwahl_diagramme.py
Benoetigt: graphviz (Python-Paket) und das Kommandozeilenwerkzeug 'dot'
"""
from __future__ import annotations
import os
import graphviz
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, CYAN, VIOLETT, GRUEN, AMBER = (
"#4338ca", "#0891b2", "#7c3aed", "#059669", "#b45309")
TINT = {INDIGO: "#eef2ff", CYAN: "#ecfeff", VIOLETT: "#f5f3ff",
GRUEN: "#ecfdf5", AMBER: "#fffbeb"}
def neuer_graph(titel: str) -> graphviz.Digraph:
punkt = graphviz.Digraph(format="svg")
punkt.attr(rankdir="TB", splines="polyline", nodesep="0.35", ranksep="0.45",
fontname="DejaVu Sans", label=titel, labelloc="t",
fontsize="13", bgcolor="transparent")
punkt.attr("node", fontname="DejaVu Sans", fontsize="10")
punkt.attr("edge", fontname="DejaVu Sans", fontsize="9", color="#5c6380")
return punkt
def frage(punkt: graphviz.Digraph, name: str, text: str, farbe: str = INDIGO) -> None:
punkt.node(name, text, shape="diamond", style="filled",
fillcolor=TINT[farbe], color=farbe, penwidth="1.6",
margin="0.08,0.02")
def empfehlung(punkt: graphviz.Digraph, name: str, text: str, farbe: str) -> None:
punkt.node(name, text, shape="box", style="filled,rounded",
fillcolor=TINT[farbe], color=farbe, penwidth="2.0",
margin="0.16,0.09")
def schreibe(punkt: graphviz.Digraph, name: str) -> None:
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
punkt.format = endung
pfad = punkt.render(os.path.join(BILDER, name), cleanup=True)
print(f"geschrieben: {pfad}")
# --- Teil II: deterministische Optimierung ----------------------------------
def teil2() -> None:
punkt = neuer_graph("Welches Werkzeug passt zu meinem Problem? (Teil II)")
frage(punkt, "f1", "Fahrzeuge, Depots,\nZeitfenster?")
frage(punkt, "f2", "Ja/Nein-Entscheidungen,\nZuordnungen, Reihenfolgen?")
frage(punkt, "f3", "Zielfunktion und alle\nNebenbedingungen linear?")
frage(punkt, "f4", "Logische Regeln\n(wenn-dann, entweder-oder)\noder Zeitplanung?")
frage(punkt, "f5", "Nur Fluss oder\nZuordnung im Netzwerk?")
empfehlung(punkt, "routing", "OR-Tools Routing\n(Kapitel Graphen)", AMBER)
empfehlung(punkt, "cpsat", "CP-SAT\n(Kapitel CP-SAT)", VIOLETT)
empfehlung(punkt, "milp", "MILP: HiGHS / CP-SAT\n(Kapitel MILP)", CYAN)
empfehlung(punkt, "netz", "Spezialalgorithmus:\nMin-Cost-Flow,\nUngarische Methode", GRUEN)
empfehlung(punkt, "lp", "LP: scipy.linprog\noder highspy\n(Kapitel LP)", INDIGO)
empfehlung(punkt, "teil3", "weiter in Teil III", AMBER)
punkt.edge("f1", "routing", "ja")
punkt.edge("f1", "f2", "nein")
punkt.edge("f2", "f4", "ja")
punkt.edge("f2", "f3", "nein")
punkt.edge("f4", "cpsat", "ja")
punkt.edge("f4", "milp", "nein\n(Fixkosten,\nKardinalität)")
punkt.edge("f3", "f5", "ja")
punkt.edge("f3", "teil3", "nein")
punkt.edge("f5", "netz", "ja")
punkt.edge("f5", "lp", "nein")
schreibe(punkt, "teil2_solverwahl")
# --- Teil III: Nichtlinearitaet, Unsicherheit, Dynamik ----------------------
def teil3() -> None:
punkt = neuer_graph("Welches Werkzeug passt zu meinem Problem? (Teil III)")
frage(punkt, "g1", "Sind die Daten sicher\nbekannt?")
frage(punkt, "g2", "Entscheidung fällt in\nmehreren Stufen\nüber die Zeit?")
frage(punkt, "g3", "Wahrscheinlichkeiten\nbekannt?")
frage(punkt, "g4", "Zielfunktion konvex?\n(Quadrate, Normen,\nlog/exp konvex kombiniert)")
frage(punkt, "g5", "Quadratisch mit\nlinearen Nebenbedingungen?")
empfehlung(punkt, "dp", "Dynamische Programmierung\n(Bellman)", AMBER)
empfehlung(punkt, "stoch", "Zweistufige stochastische\nOptimierung, Szenarien", VIOLETT)
empfehlung(punkt, "robust", "Robuste Optimierung\n(Worst Case)", VIOLETT)
empfehlung(punkt, "qp", "QP über CVXPY\n(OSQP, Clarabel)", CYAN)
empfehlung(punkt, "konvex", "CVXPY\n— prüft Konvexität selbst", GRUEN)
empfehlung(punkt, "nichtkonvex", "scipy.optimize.minimize\nmit mehreren Startpunkten\n— nur lokales Optimum!", AMBER)
punkt.edge("g1", "g4", "ja")
punkt.edge("g1", "g2", "nein")
punkt.edge("g2", "dp", "ja")
punkt.edge("g2", "g3", "nein")
punkt.edge("g3", "stoch", "ja")
punkt.edge("g3", "robust", "nein,\nnur Bandbreiten")
punkt.edge("g4", "g5", "ja")
punkt.edge("g4", "nichtkonvex", "nein")
punkt.edge("g5", "qp", "ja")
punkt.edge("g5", "konvex", "nein")
schreibe(punkt, "teil3_solverwahl")
if __name__ == "__main__":
teil2()
teil3()

View file

@ -0,0 +1,338 @@
#!/usr/bin/env python3
# erzeuge_titelseite.py
"""
Baut die Titelseite bilder_04/titelseite.svg (und die HTML-Kopie
OR_HTML_04/bilder_04/titelseite.svg) als SVG mit matplotlib. Das Datum im Fußband
('Stand: <datum>') wird dynamisch aus datetime.now() gesetzt, damit es nach
jedem Build den aktuellen Stand zeigt.
Die Koordinaten unten sind SVG-Punkte (595.44 x 841.68, A4 @ 72 dpi). Die
y-Achse wird invertiert (set_ylim(841.68, 0)), sodass y von oben zählt und
die Werte direkt mit der alten, von Hand erzeugten titelseite.svg
übereinstimmen.
Design (aus der Vorgängerversion abgeleitet):
- weißer Hintergrund
- 5 farbige Akzentbalken oben
- 7 konzentrische Ringe oben rechts (indigo, transparent)
- violette Trennlinie unter dem Untertitel
- grüne, konkave 'Optimierungskurve' mit rotem Stern-Marker (Optimum)
- große Solver-Karte (hell, violetter Rand) mit vier kleinen farbigen
Solver-Karten (HiGHS indigo, SciPy grün, Google OR-Tools cyan, CVXPY violett)
- indigo Fußband unten mit Autor (weiß) und Stand-Datum (hellviolett)
Aufruf:
python3 bilder_04/erzeuge_titelseite.py
(wird vom Build-Skript build_version_03.py vor dem PDF-/HTML-Bau aufgerufen).
"""
import os
from datetime import datetime
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyBboxPatch, Rectangle
# Reproduzierbare SVG-Ausgabe. Ohne diese beiden Vorkehrungen unterscheiden sich
# zwei Laeufe - und sogar die zwei Kopien AUS DEMSELBEN Lauf - in zwei Punkten:
# * svg.hashsalt: matplotlib wuerfelt sonst die clip-path-IDs je Figure neu.
# * metadata={"Date": None} beim Speichern: sonst steht der Zeitstempel des
# Schreibvorgangs (auf die Mikrosekunde) in den SVG-Metadaten.
# Beides ist unsichtbar, macht aber jeden git-diff unlesbar und die beiden
# Kopien bilder_04/ und OR_HTML_04/bilder_04/ dauerhaft verschieden. Die Datei
# aendert sich jetzt nur noch, wenn sich das SICHTBARE Datum im Fussband
# aendert - also einmal pro Tag.
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
# Hardcodierte deutsche Monatsnamen statt strftime("%B") - so ist das Datum
# unabhängig von der gesetzten Locale (de_DE.UTF-8) immer korrekt geschrieben,
# auch in CI-/Docker-/Headless-Umgebungen ohne deutsche Locale.
MONATE = ["Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"]
# A4 bei 72 dpi -> 595.44 x 841.68 pt (matplotlib figure-Koordinaten).
BREITE, HOEHE = 595.44, 841.68
# Geometrie des Mittelteils. _design() zeichnet danach, _texte() setzt danach -
# beide lesen aus DIESEN Werten, damit Kasten und Text nie auseinanderlaufen.
# Trennlinie und Kartenoberkante sind gegenueber dem urspruenglichen Entwurf
# (336.7 / 368.7) nach oben gerueckt: Der Untertitel braucht seit der
# Titelaenderung nur noch zwei statt vier Zeilen, und der gewonnene Platz kommt
# der Solver-Karte zugute, die jetzt zwei Absaetze traegt.
TRENNLINIE_Y = 308.0
KARTE_X0, KARTE_BREITE = 64.30752, 466.82496
KARTE_Y0, KARTE_Y1 = 340.0, 481.44096
# Solver-Karten (x0, Breite, Farbe, Label) - bewusst NICHT gleich breit:
# "Google OR-Tools" ist deutlich laenger als die anderen Labels und liefe bei
# einheitlicher Breite ueber die Karte hinaus - da das Label weiss ist, wuerde
# der ueberstehende Teil auf dem weissen Seitenhintergrund unsichtbar. Reihe
# beginnt bei x=71.4528 (linker Rand wie Titel/Untertitel) und endet bei
# x=523.9872 (wie die violette Trennlinie), dazwischen 3 Luecken von je 10pt.
KARTEN_Y0, KARTEN_Y1 = 494.06616, 541.20024
KARTEN = [
(71.4528, 94.0, "#4338ca", "HiGHS"),
(175.4528, 94.0, "#059669", "SciPy"),
(279.4528, 140.5344, "#0891b2", "Google OR-Tools"),
(429.9872, 94.0, "#7c3aed", "CVXPY"),
]
def datum_string() -> str:
"""Heutiges Datum als '5. September 2026' (locale-unabhängig)."""
n = datetime.now()
return f"{n.day}. {MONATE[n.month - 1]} {n.year}"
def _ring(ax, x, y, r):
ax.add_patch(Circle((x, y), r, fill=False, ec="#4338ca", alpha=0.1,
lw=1.4, clip_on=True))
def _design(ax):
# Weißer Hintergrund.
ax.add_patch(Rectangle((0, 0), BREITE, HOEHE, facecolor="white",
edgecolor="none", zorder=0))
# 5 farbige Akzentbalken oben (Leiste), y 34.51..46.29, je 90.51 breit.
balken_farben = ["#4338ca", "#0891b2", "#7c3aed", "#059669", "#d97706"]
for i, farbe in enumerate(balken_farben):
ax.add_patch(Rectangle((71.4528 + i * 90.50688, 34.50888),
90.50688, 11.78352, facecolor=farbe,
edgecolor="none", zorder=1))
# 7 konzentrische Ringe oben rechts, Zentrum (553.76, 84.17).
for r in (42.084, 77.154, 112.224, 147.294, 182.364, 217.434, 252.504):
_ring(ax, 553.7592, 84.168, r)
# Große Solver-Karte (abgerundet): (64.31, 368.66) bis (531.13, 481.44),
# Radius ~11.91 (Q-Offset in der Original-SVG).
ax.add_patch(FancyBboxPatch(
(KARTE_X0, KARTE_Y0), KARTE_BREITE, KARTE_Y1 - KARTE_Y0,
boxstyle="round,pad=0,rounding_size=11.9098",
facecolor="#f5f3ff", edgecolor="#7c3aed", lw=1.6, zorder=1))
# Vier farbige Solver-Karten (HiGHS, SciPy, Google OR-Tools, CVXPY),
# individuelle Breiten (siehe KARTEN), y 494.07..541.20.
for x0, breite, farbe, _label in KARTEN:
ax.add_patch(FancyBboxPatch(
(x0, KARTEN_Y0), breite, KARTEN_Y1 - KARTEN_Y0,
boxstyle="round,pad=0,rounding_size=11.9098",
facecolor=farbe, edgecolor="none", zorder=2))
# Violette Trennlinie unter dem Untertitel, x 71.45..523.99.
ax.plot([71.4528, 523.9872], [TRENNLINIE_Y, TRENNLINIE_Y], color="#7c3aed",
lw=2.5, solid_capstyle="butt", zorder=3)
# Indigo Fußband unten: (0, 757.51) bis (595.44, 841.68).
ax.add_patch(Rectangle((0, 757.512), BREITE, HOEHE - 757.512,
facecolor="#4338ca", edgecolor="none", zorder=1))
# Grüne 'Optimierungskurve' (konkav, abflachend) von (71.45, 673.34) nach
# (523.99, 639.68) - approximiert die dekorative Kurve der Vorgängerversion
# (dort 99 geglättete Stützpunkte, hier eine konkave Wurzelfunktion, die
# denselben visuellen Eindruck erzeugt).
import numpy as np
xs = np.linspace(71.4528, 523.9872, 100)
t = (xs - 71.4528) / (523.9872 - 71.4528)
ys = 673.344 - 33.664 * np.sqrt(t) # konkav: steiler Start, flaches Ende
ax.plot(xs, ys, color="#059669", lw=3, solid_capstyle="butt", zorder=3)
# Roter Stern-Marker ('Optimum') bei (446.28, 641.67) auf der Kurve.
ax.plot([446.279273], [641.670502], marker="*", color="#e11d48",
markersize=12, mec="#e11d48", mfc="#e11d48", zorder=4)
# --- Textumbruch: gemessen statt geschaetzt -------------------------------
# Bis zur Titelaenderung standen alle Textzeilen mit von Hand vermessenen
# Koordinaten hier - uebernommen aus der urspruenglichen, handgezeichneten SVG.
# Das traegt genau so lange, wie sich der Text nicht aendert: Der neue Titel ist
# knapp dreimal so lang wie der alte, der Untertitel mehr als doppelt so lang.
# Deshalb werden Umbruch und Schriftgroesse jetzt GEMESSEN. Wer den Titel das
# naechste Mal aendert, aendert nur noch die Zeichenketten weiter unten.
def _textbreite(ax, text, groesse, fett=False) -> float:
"""Breite eines Textes in Seitenpunkten - ueber den echten Renderer."""
marke = ax.text(0, 0, text, fontsize=groesse,
fontweight="bold" if fett else "normal")
kasten = marke.get_window_extent(
renderer=ax.figure.canvas.get_renderer()).transformed(
ax.transData.inverted())
marke.remove()
return abs(kasten.width)
def _umbrechen(ax, text, groesse, maxbreite, fett=False) -> list[str]:
"""Bricht 'text' so um, dass keine Zeile breiter als 'maxbreite' wird."""
zeilen, zeile = [], ""
for wort in text.split():
probe = f"{zeile} {wort}".strip()
if not zeile or _textbreite(ax, probe, groesse, fett) <= maxbreite:
zeile = probe
else:
zeilen.append(zeile)
zeile = wort
if zeile:
zeilen.append(zeile)
return zeilen
def _setze_block(ax, zeilen, x, y0, groesse, durchschuss, **stil) -> float:
"""Setzt die Zeilen ab Grundlinie y0 und liefert die Grundlinie danach."""
for i, zeile in enumerate(zeilen):
ax.text(x, y0 + i * groesse * durchschuss, zeile,
fontsize=groesse, zorder=5, **stil)
return y0 + (len(zeilen) - 1) * groesse * durchschuss
# --- Die Texte der Titelseite ----------------------------------------------
# Nur diese Zeichenketten aendern, wenn das Buch anders heissen soll. Der Text
# stammt aus Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md; die beiden
# Absaetze KOMPENDIUM und ANWENDUNGEN
# stehen gemeinsam in der grossen Solver-Karte.
#
# Im TITEL steht ein \n: An dieser Stelle wird bewusst umbrochen. Ohne ihn
# wuerde automatisch umbrochen und "mit Python" auseinandergerissen. Die Breite
# jeder Zeile wird in beiden Faellen geprueft.
#
# Warum drei Zeilen und nicht zwei: "Entscheidungsfindung" ist bei 34 pt schon
# 429 pt breit und damit fast so breit wie der ganze Satzspiegel (453 pt). Jede
# zweizeilige Aufteilung braeuchte mindestens 649 pt - das passt bei keiner
# lesbaren Schriftgroesse.
TITEL = "Optimierte\nEntscheidungsfindung\nmit Python"
UNTERTITEL = ("Von der mathematischen Modellierung zu praktischen "
"intelligenten Lösungen für Betrieb, Technik und Finanzmärkte")
KOMPENDIUM = ("Ein praxisorientiertes Kompendium für Entscheider mit "
"Constraint Programming, Vektor- und Matrixmethoden sowie "
"HiGHS, SciPy, Google OR-Tools und CVXPY.")
ANWENDUNGEN = ("Von Personaleinsatz-, Schicht- und Vertretungsplänen, "
"Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung "
"über Energieverteilung "
"und Ressourcenplanung bis zur Portfoliooptimierung und besseren "
"Entscheidungen an den Finanzmärkten mit Python modellieren, "
"optimieren und belegbar fundierte Entscheidungen treffen.")
AUTOR = "Autor / Herausgeber: Dieter Schlüter"
# Satzspiegel: linker Rand und rechte Kante wie Trennlinie und Kartenreihe.
LINKS, RECHTS = 71.4528, 523.9872
SATZBREITE = RECHTS - LINKS
# Schriftgroessen und Durchschuss (Faktor auf die Schriftgroesse). Nachgemessen,
# nicht geraten: Bei 34 pt ist "Entscheidungsfindung" 429 pt breit und bleibt
# damit im Satzspiegel von 453 pt; bei 36 pt waeren es 454 pt - eines zu viel.
TITEL_GROESSE, TITEL_DURCHSCHUSS = 34, 1.40
# Untertitel 14 statt 15 pt: Bei 15 pt braucht er drei Zeilen, und die dritte
# traegt nur noch das Wort "Finanzmärkte" - eine Hurenkind-Zeile mitten auf dem
# Titelblatt. Bei 14 pt sind es zwei ausgeglichene Zeilen.
UNTER_GROESSE, UNTER_DURCHSCHUSS = 14, 1.23
# Kartentext 11.5 statt 12 pt: Der Text ist auf neun Zeilen gewachsen; bei 12 pt
# blieben nur noch 9 pt Luft ueber und unter dem Block, und der Text geriete in
# die abgerundeten Kartenecken (Radius 11.9 pt).
KOMP_GROESSE, KOMP_DURCHSCHUSS = 11.5, 1.27
KOMP_BREITE = 440.0 # nutzbare Textbreite in der Karte
# Anker des Satzes.
TITEL_START = 135.0 # erste Grundlinie des Haupttitels
ABSTAND_TITEL_UNTER = 42.0 # Luft zwischen Titel- und Untertitelblock
def _texte(ax, datum):
# Haupttitel: die letzte Zeile ("mit Python") in Anthrazit statt Indigo -
# so bleibt die Zweifarbigkeit des urspruenglichen Entwurfs erhalten, in
# dem "Operations Research" indigo und "mit Python" dunkel gesetzt war.
# Ein \n im TITEL heisst: hier wird umbrochen. Sonst automatisch.
if "\n" in TITEL:
titelzeilen = TITEL.split("\n")
zu_breit = [z for z in titelzeilen
if _textbreite(ax, z, TITEL_GROESSE, True) > SATZBREITE]
if zu_breit:
raise SystemExit(
f"Titelblatt: Diese Titelzeile passt bei {TITEL_GROESSE} pt "
f"nicht in den Satzspiegel ({SATZBREITE:.0f} pt): {zu_breit[0]!r}")
else:
titelzeilen = _umbrechen(ax, TITEL, TITEL_GROESSE, SATZBREITE, fett=True)
for i, zeile in enumerate(titelzeilen):
ax.text(LINKS, TITEL_START + i * TITEL_GROESSE * TITEL_DURCHSCHUSS,
zeile, fontsize=TITEL_GROESSE, fontweight="bold", zorder=5,
color="#1a1a2e" if i == len(titelzeilen) - 1 else "#4338ca")
titel_ende = TITEL_START + (len(titelzeilen) - 1) * TITEL_GROESSE * TITEL_DURCHSCHUSS
# Untertitel direkt darunter; die Zeilenzahl ergibt sich aus dem Umbruch.
unterzeilen = _umbrechen(ax, UNTERTITEL, UNTER_GROESSE, SATZBREITE)
unter_ende = _setze_block(ax, unterzeilen, LINKS,
titel_ende + ABSTAND_TITEL_UNTER,
UNTER_GROESSE, UNTER_DURCHSCHUSS, color="#334155")
# Sicherung: Der Untertitel darf die Trennlinie nicht beruehren. Lieber ein
# Abbruch beim Erzeugen als ein Titelblatt, auf dem Text auf der Linie liegt.
if unter_ende > TRENNLINIE_Y - 8:
raise SystemExit(
f"Titelblatt: Der Untertitel reicht bis y={unter_ende:.1f} und "
f"kollidiert mit der Trennlinie bei y={TRENNLINIE_Y}. "
f"Schriftgroesse oder Startpunkt anpassen.")
# KOMPENDIUM-Label
ax.text(LINKS, 84.168, "K O M P E N D I U M",
fontsize=13, color="#0891b2", zorder=5)
# Die beiden Absaetze in der grossen Solver-Karte, durch eine Leerzeile
# getrennt und als Block vertikal zentriert. Zentriert gesetzt statt links,
# weil die Karte breiter ist als der Satzspiegel.
kartenmitte_x = KARTE_X0 + KARTE_BREITE / 2
kartenzeilen = (_umbrechen(ax, KOMPENDIUM, KOMP_GROESSE, KOMP_BREITE)
+ [""]
+ _umbrechen(ax, ANWENDUNGEN, KOMP_GROESSE, KOMP_BREITE))
hoehe = (len(kartenzeilen) - 1) * KOMP_GROESSE * KOMP_DURCHSCHUSS
start = (KARTE_Y0 + KARTE_Y1) / 2 - hoehe / 2 + KOMP_GROESSE * 0.35
# Sicherung: Der Block darf nicht ueber den Kartenrand hinauslaufen.
if start - KOMP_GROESSE < KARTE_Y0 or start + hoehe > KARTE_Y1 - 4:
raise SystemExit(
f"Titelblatt: Der Kartentext ({len(kartenzeilen)} Zeilen) passt "
f"nicht in die Karte ({KARTE_Y1 - KARTE_Y0:.0f} pt hoch). "
f"Schriftgroesse verkleinern oder Karte vergroessern.")
_setze_block(ax, kartenzeilen, kartenmitte_x, start,
KOMP_GROESSE, KOMP_DURCHSCHUSS, color="#1a1a2e", ha="center")
# Autor (im Fußband, weiß)
ax.text(LINKS, 793.402715, AUTOR,
fontsize=14, color="#ffffff", zorder=5)
# text_7: Stand-Datum (im Fußband, hellviolett) - DYNAMISCH
ax.text(71.4528, 818.05749, f"Stand: {datum}",
fontsize=12, color="#c7d2fe", zorder=5)
# Solver-Namen, zentriert in ihrer Karte (weiß). ha/va="center"
# macht die Zentrierung unabhängig von Schriftmetriken - robuster als
# von Hand vermessene x/y-Offsets.
mitte_y = (KARTEN_Y0 + KARTEN_Y1) / 2
for x0, breite, _farbe, label in KARTEN:
ax.text(x0 + breite / 2, mitte_y, label,
fontsize=13, color="#ffffff", ha="center", va="center",
zorder=5)
def main() -> int:
basis = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ausgaben = [
os.path.join(basis, "bilder_04", "titelseite.svg"),
os.path.join(basis, "OR_HTML_04", "bilder_04", "titelseite.svg"),
]
d = datum_string()
fig = plt.figure(figsize=(8.27, 11.69))
ax = fig.add_axes([0, 0, 1, 1])
ax.set_xlim(0, BREITE)
ax.set_ylim(HOEHE, 0) # invertiert: y zählt von oben (wie SVG)
ax.axis("off")
_design(ax)
_texte(ax, d)
for pfad in ausgaben:
os.makedirs(os.path.dirname(pfad), exist_ok=True)
fig.savefig(pfad, format="svg", metadata={"Date": None})
plt.close(fig)
print(f"Titelseite erzeugt ({d}): {', '.join(ausgaben)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,165 @@
#!/usr/bin/env python3
# erzeuge_var_cvar.py
"""
Erzeugt das VaR/CVaR-Diagramm zum Kapitel CVaR:
bilder_04/kap_cvar_var_vergleich.svg statisch, fuer das PDF
bilder_04/kap_cvar_var_vergleich.png Rasterfassung
Gezeigt wird die Verteilung simulierter Tagesrenditen mit beiden Kennzahlen:
Der VaR ist die SCHWELLE, unter die nur 5 % der Tage fallen; der CVaR ist der
MITTELWERT genau dieser 5 %. Der Abstand zwischen beiden Linien ist die
Information, die der VaR wegwirft.
Zur Instanz: Die Renditen stammen aus einer t-Verteilung mit drei
Freiheitsgraden, auf Standardabweichung 1 normiert und dann auf eine realistische
Tagesvolatilitaet skaliert - dasselbe Fat-Tail-Modell, mit dem Teil 1 von
VaR_CVaR_Demo.py rechnet. Die beiden Kennzahlen werden mit den Funktionen aus
demselben Programm bestimmt, insbesondere der CVaR ueber Rockafellar-Uryasev
statt ueber den naheliegenden, aber falschen Mittelwert aller Werte jenseits des
VaR.
Die Zahlen, die das Skript ausgibt, stehen so auch im Kapiteltext. Wer die
Instanz aendert, muss beides nachziehen - deshalb druckt das Skript sie.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_var_cvar.py
Benoetigt: numpy, scipy, matplotlib
"""
from __future__ import annotations
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from scipy import stats
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
INDIGO, AMBER, ROT = "#4338ca", "#b45309", "#be123c"
TAGE = 20_000 # simulierte Handelstage
FREIHEITSGRADE = 3 # Fat-Tail-Modell des Kapitels
TAGESVOLA = 0.014 # 1,4 % Tagesschwankung - realistisch fuer ein Aktienportfolio
TAGESDRIFT = 0.0003 # leicht positiver Erwartungswert
ALPHA = 0.95
# --- Kennzahlen: wortgleich zu VaR_CVaR_Demo.py -----------------------------
def var_quantil(verluste: np.ndarray, alpha: float = ALPHA) -> float:
"""VaR = Quantil der Verlustverteilung (Verluste positiv, Gewinne negativ)."""
return float(np.quantile(verluste, alpha))
def cvar_rockafellar(verluste: np.ndarray, alpha: float = ALPHA) -> float:
"""CVaR ueber min_gamma { gamma + 1/(1-alpha) * E[max(Verlust - gamma, 0)] }.
Der naheliegende Weg "Mittelwert aller Werte >= VaR" ist falsch, sobald die
Verteilung Atome hat. Hier hat sie keine, aber die Formel bleibt dieselbe
wie im Buchprogramm - und nur so ist der Wert im Bild derselbe wie dort.
"""
kandidaten = np.unique(verluste)
return float(min(g + np.mean(np.maximum(verluste - g, 0.0)) / (1.0 - alpha)
for g in kandidaten))
def renditen(saat: int = 2026) -> np.ndarray:
"""Tagesrenditen aus dem Fat-Tail-Modell des Kapitels."""
rng = np.random.default_rng(saat)
verteilung = stats.t(df=FREIHEITSGRADE)
roh = verteilung.rvs(size=TAGE, random_state=rng) / verteilung.std()
return TAGESDRIFT + TAGESVOLA * roh
AUSSCHNITT = 8.0 # Prozent - der sichtbare Bereich der x-Achse
def zeichne(werte: np.ndarray, var: float, cvar: float) -> None:
"""Zeichnet die Verteilung mit beiden Kennzahlen.
Die x-Achse ist bewusst auf +/- 8 % beschnitten. Eine t-Verteilung mit drei
Freiheitsgraden hat keine endliche Woelbung: Einzelne Tage liegen bei +70 %,
und ungeschnitten quetscht sich die gesamte Verteilung zu einem Strich
zusammen. Wie viele Tage ausserhalb liegen, steht im Bild - sie sind nicht
verschwiegen, sondern gerade der Punkt.
"""
figur, achse = plt.subplots(figsize=(8.0, 4.6))
prozent = werte * 100
sichtbar = prozent[np.abs(prozent) <= AUSSCHNITT]
achse.hist(sichtbar, bins=90, color=INDIGO, alpha=0.75,
edgecolor="white", linewidth=0.3)
achse.set_xlim(-AUSSCHNITT, AUSSCHNITT)
achse.axvspan(-AUSSCHNITT, -var * 100, color=ROT, alpha=0.08, zorder=0)
achse.axvline(-var * 100, color=AMBER, linewidth=2.0, linestyle="--")
achse.axvline(-cvar * 100, color=ROT, linewidth=2.0)
def komma(wert: float, stellen: int = 2) -> str:
return f"{wert:.{stellen}f}".replace(".", ",")
# Platz oberhalb der Verteilung schaffen, damit keine Beschriftung in den
# Balken liegt - die Aussage steht sonst buchstaeblich im Weg.
hoehe = achse.get_ylim()[1]
achse.set_ylim(0, hoehe * 1.30)
achse.annotate(f"VaR 95 % = {komma(var * 100)} %\ndie Schwelle",
(-var * 100, hoehe * 1.24), textcoords="offset points",
xytext=(8, 0), fontsize=9, color=AMBER, va="top")
achse.annotate(f"CVaR 95 % = {komma(cvar * 100)} %\nder Mittelwert dahinter",
(-cvar * 100, hoehe * 1.24), textcoords="offset points",
xytext=(-8, 0), fontsize=9, color=ROT, va="top", ha="right")
anteil = float(np.mean(werte <= -var))
achse.annotate(f"die schlechtesten\n{anteil * 100:.0f} % der Tage",
((-AUSSCHNITT - var * 100) / 2, hoehe * 0.55),
fontsize=8.5, color="#475569", ha="center")
draussen = int(np.sum(np.abs(prozent) > AUSSCHNITT))
schlimmster = float(prozent.min())
achse.annotate(f"{draussen} Tage liegen außerhalb dieses Ausschnitts,\n"
f"der schlechteste bei {komma(schlimmster, 1)} %.\n"
f"Genau sie zählt der VaR nicht mit.",
xy=(0.985, 0.80), xycoords="axes fraction", fontsize=8.5,
color="#475569", ha="right", va="top")
achse.set_xlabel("Tagesrendite des Portfolios (%)")
achse.set_ylabel("Häufigkeit (von 20 000 Tagen)")
achse.set_title("Der VaR ist eine Schwelle, der CVaR ein Mittelwert",
fontsize=11)
achse.grid(axis="y", linestyle=":", alpha=0.45)
for rand in ("top", "right"):
achse.spines[rand].set_visible(False)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap_cvar_var_vergleich.{endung}")
figur.savefig(pfad, format=endung, dpi=160,
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)
if __name__ == "__main__":
werte = renditen()
verluste = -werte
var = var_quantil(verluste)
cvar = cvar_rockafellar(verluste)
schlimmster = float(verluste.max())
print(f"{TAGE} simulierte Tage, t(df={FREIHEITSGRADE}), "
f"Tagesvola {TAGESVOLA * 100:.1f} %")
print(f"VaR 95 % = {var * 100:.2f} % (Schwelle)")
print(f"CVaR 95 % = {cvar * 100:.2f} % (Mittel der schlechtesten 5 %)")
print(f"schlimmster Einzeltag: {schlimmster * 100:.2f} %")
print(f"Verhaeltnis CVaR/VaR: {cvar / var:.2f}")
zeichne(werte, var, cvar)

View file

@ -0,0 +1,191 @@
#!/usr/bin/env python3
# erzeuge_vrp_touren.py
"""
Erzeugt die Tourenkarte zum Graphen-Kapitel:
bilder_04/kap_graphen_vrp_touren.svg / .png
Das Vorgaengerbild war ein Schema: zwei Touren, vier Kunden, Zeitfenster in
Uhrzeiten. Die Instanz des Kapitels hat 16 Kunden, vier Fahrzeuge und
Zeitfenster in Minuten - das Bild zeigte also die Bauform, nicht den Fall.
Hier wird die Instanz aus VRP_Flotten_Routing.py geloest und die Loesung als
Karte gezeichnet: Depot, Kunden (Punktgroesse = Bedarf), die vier Touren in
vier Farben, und in der Legende je Fahrzeug Strecke und Auslastung.
ZUR REPRODUZIERBARKEIT: Das Buchprogramm gibt der Suche ein ZEITBUDGET von
fuenf Sekunden. Ein Zeitbudget ist auf einer anderen Maschine ein anderes
Rechenbudget, das Ergebnis also nicht garantiert identisch. Fuer das Bild wird
deshalb zusaetzlich eine feste Loesungszahl als Abbruchkriterium gesetzt und
die Gesamtstrecke ausgegeben - weicht sie vom abgedruckten Lauf ab, faellt es
sofort auf, statt still ins Buch zu wandern.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_vrp_touren.py
Benoetigt: numpy, matplotlib, ortools
"""
from __future__ import annotations
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from ortools.constraint_solver import pywrapcp, routing_enums_pb2
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BILDER = os.path.join(BASIS, "bilder_04")
TOURFARBEN = ["#4338ca", "#0891b2", "#b45309", "#059669"]
# --- Instanz: wortgleich zu VRP_Flotten_Routing.py --------------------------
SERVICEZEIT = 10
WARTEZEIT_MAX = 60
SCHICHTLAENGE = 600
SAAT = 42
ZEITBUDGET_S = 5
LOESUNGSGRENZE = 300 # zusaetzliches, maschinenunabhaengiges Kriterium
ZEITFENSTER = [
(0, SCHICHTLAENGE),
(30, 120), (60, 180), (100, 240), (150, 300),
(60, 180), (120, 240), (200, 360), (300, 450),
(180, 300), (240, 360), (300, 480), (360, 500),
(60, 200), (120, 300), (240, 400), (300, 550),
]
BEDARFE = [0, 2, 3, 1, 4, 2, 2, 3, 1, 2, 4, 3, 2, 1, 2, 3, 2]
KAPAZITAETEN = [10, 10, 10, 10]
def erzeuge_daten():
anzahl_orte = len(ZEITFENSTER)
rng = np.random.default_rng(SAAT)
koordinaten = rng.random((anzahl_orte, 2)) * 100
distanz = np.zeros((anzahl_orte, anzahl_orte), dtype=int)
for i in range(anzahl_orte):
for j in range(anzahl_orte):
distanz[i][j] = int(np.linalg.norm(koordinaten[i] - koordinaten[j]))
return koordinaten, distanz
def loese(distanz):
"""CVRPTW mit denselben Einstellungen wie im Buchprogramm."""
manager = pywrapcp.RoutingIndexManager(len(distanz), len(KAPAZITAETEN), 0)
routing = pywrapcp.RoutingModel(manager)
def zeit_callback(von_index, nach_index):
von = manager.IndexToNode(von_index)
nach = manager.IndexToNode(nach_index)
service = SERVICEZEIT if von != 0 else 0
return int(distanz[von][nach]) + service
zeit_index = routing.RegisterTransitCallback(zeit_callback)
routing.SetArcCostEvaluatorOfAllVehicles(zeit_index)
def bedarf_callback(von_index):
return BEDARFE[manager.IndexToNode(von_index)]
bedarf_index = routing.RegisterUnaryTransitCallback(bedarf_callback)
routing.AddDimensionWithVehicleCapacity(
bedarf_index, 0, KAPAZITAETEN, True, "Kapazitaet")
routing.AddDimension(zeit_index, WARTEZEIT_MAX, SCHICHTLAENGE, False, "Zeit")
zeit_dimension = routing.GetDimensionOrDie("Zeit")
for ort, (fruehestens, spaetestens) in enumerate(ZEITFENSTER):
zeit_dimension.CumulVar(manager.NodeToIndex(ort)).SetRange(
fruehestens, spaetestens)
parameter = pywrapcp.DefaultRoutingSearchParameters()
parameter.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
parameter.local_search_metaheuristic = (
routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
parameter.time_limit.seconds = ZEITBUDGET_S
parameter.solution_limit = LOESUNGSGRENZE
loesung = routing.SolveWithParameters(parameter)
if not loesung:
raise SystemExit("Keine zulaessige Routenfuehrung gefunden.")
touren = []
for fahrzeug in range(len(KAPAZITAETEN)):
index = routing.Start(fahrzeug)
if routing.IsEnd(loesung.Value(routing.NextVar(index))):
continue
stationen, fracht, strecke = [], 0, 0
while not routing.IsEnd(index):
knoten = manager.IndexToNode(index)
stationen.append(knoten)
fracht += BEDARFE[knoten]
vorher = index
index = loesung.Value(routing.NextVar(index))
strecke += int(distanz[manager.IndexToNode(vorher)][
manager.IndexToNode(index)])
stationen.append(0)
touren.append({"stationen": stationen, "fracht": fracht,
"strecke": strecke,
"kapazitaet": KAPAZITAETEN[fahrzeug]})
return touren
def zeichne(koordinaten, touren) -> None:
figur, achse = plt.subplots(figsize=(7.6, 6.2))
gesamt = sum(t["strecke"] for t in touren)
for nummer, (tour, farbe) in enumerate(zip(touren, TOURFARBEN), start=1):
weg = koordinaten[tour["stationen"]]
achse.plot(weg[:, 0], weg[:, 1], "-", color=farbe, linewidth=1.8,
alpha=0.85, zorder=2,
label=f"Fahrzeug {nummer}: {tour['strecke']} km, "
f"{tour['fracht']}/{tour['kapazitaet']} Einheiten")
for ort in range(1, len(koordinaten)):
achse.scatter(*koordinaten[ort], s=30 + 26 * BEDARFE[ort],
color="#334155", zorder=3)
achse.annotate(f"K{ort}", koordinaten[ort], textcoords="offset points",
xytext=(7, 5), fontsize=7.5, color="#475569")
achse.scatter(*koordinaten[0], marker="s", s=190, color="#be123c", zorder=4)
achse.annotate("Depot", koordinaten[0], textcoords="offset points",
xytext=(9, 7), fontsize=9.5, color="#be123c",
fontweight="bold")
achse.set_xlabel("km")
achse.set_ylabel("km")
achse.set_title(f"Vier Touren für 16 Kunden mit Zeitfenstern — "
f"{gesamt} km insgesamt\n"
"Punktgröße = Liefermenge", fontsize=10.5)
achse.grid(linestyle=":", alpha=0.4)
achse.legend(fontsize=8, loc="upper center", bbox_to_anchor=(0.5, -0.11),
ncol=2, frameon=False)
for seite in ("top", "right"):
achse.spines[seite].set_visible(False)
figur.tight_layout()
os.makedirs(BILDER, exist_ok=True)
for endung in ("svg", "png"):
pfad = os.path.join(BILDER, f"kap_graphen_vrp_touren.{endung}")
figur.savefig(pfad, format=endung, dpi=160, bbox_inches="tight",
metadata={"Date": None} if endung == "svg" else None)
print(f"geschrieben: {pfad}")
plt.close(figur)
if __name__ == "__main__":
koordinaten, distanz = erzeuge_daten()
touren = loese(distanz)
gesamt = sum(t["strecke"] for t in touren)
for nummer, tour in enumerate(touren, start=1):
stationen = " -> ".join("Depot" if s == 0 else f"K{s}"
for s in tour["stationen"])
print(f"Fahrzeug {nummer}: {tour['strecke']:3} km, "
f"{tour['fracht']}/{tour['kapazitaet']} | {stationen}")
print(f"Gesamtstrecke: {gesamt} km")
zeichne(koordinaten, touren)

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d01 Pages: 1 -->
<svg width="507pt" height="87pt"
viewBox="7.00 7.00 500.00 79.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 75.48)">
<title>d01</title>
<!-- desk -->
<g id="node1" class="node">
<title>desk</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M117.42,-64.28C117.42,-64.28 12,-64.28 12,-64.28 6,-64.28 0,-58.28 0,-52.28 0,-52.28 0,-12 0,-12 0,-6 6,0 12,0 12,0 117.42,0 117.42,0 123.42,0 129.42,-6 129.42,-12 129.42,-12 129.42,-52.28 129.42,-52.28 129.42,-58.28 123.42,-64.28 117.42,-64.28"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-44.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">DESKRIPTIV</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Business Intelligence</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">„Was ist passiert?“</text>
</g>
<!-- pred -->
<g id="node2" class="node">
<title>pred</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M300.34,-64.28C300.34,-64.28 184.42,-64.28 184.42,-64.28 178.42,-64.28 172.42,-58.28 172.42,-52.28 172.42,-52.28 172.42,-12 172.42,-12 172.42,-6 178.42,0 184.42,0 184.42,0 300.34,0 300.34,0 306.34,0 312.34,-6 312.34,-12 312.34,-12 312.34,-52.28 312.34,-52.28 312.34,-58.28 306.34,-64.28 300.34,-64.28"/>
<text xml:space="preserve" text-anchor="start" x="203.75" y="-44.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">PRÄDIKTIV</text>
<text xml:space="preserve" text-anchor="start" x="185.38" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Data Science / ML</text>
<text xml:space="preserve" text-anchor="start" x="185.38" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">„Was wird geschehen?“</text>
</g>
<!-- desk&#45;&gt;pred -->
<g id="edge1" class="edge">
<title>desk&#45;&gt;pred</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M130.13,-32.14C139.73,-32.14 149.74,-32.14 159.65,-32.14"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="159.62,-35.29 168.62,-32.14 159.62,-28.99 159.62,-35.29"/>
</g>
<!-- pres -->
<g id="node3" class="node">
<title>pres</title>
<path fill="#7c3aed" stroke="#7c3aed" stroke-width="2" d="M472.76,-64.28C472.76,-64.28 367.34,-64.28 367.34,-64.28 361.34,-64.28 355.34,-58.28 355.34,-52.28 355.34,-52.28 355.34,-12 355.34,-12 355.34,-6 361.34,0 367.34,0 367.34,0 472.76,0 472.76,0 478.76,0 484.76,-6 484.76,-12 484.76,-12 484.76,-52.28 484.76,-52.28 484.76,-58.28 478.76,-64.28 472.76,-64.28"/>
<text xml:space="preserve" text-anchor="start" x="372.42" y="-44.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#ffffff">PRÄSKRIPTIV</text>
<text xml:space="preserve" text-anchor="start" x="368.3" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#ffffff">Operations Research</text>
<text xml:space="preserve" text-anchor="start" x="368.3" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#ffffff">„Was sollen wir tun?“</text>
</g>
<!-- pred&#45;&gt;pres -->
<g id="edge2" class="edge">
<title>pred&#45;&gt;pres</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M313.31,-32.14C322.97,-32.14 332.93,-32.14 342.7,-32.14"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="342.46,-35.29 351.46,-32.14 342.46,-28.99 342.46,-35.29"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d02 Pages: 1 -->
<svg width="447pt" height="271pt"
viewBox="7.00 7.00 439.00 264.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 260.04)">
<title>d02</title>
<!-- param -->
<g id="node1" class="node">
<title>param</title>
<path fill="#fffbeb" stroke="#d97706" stroke-width="2" d="M292.54,-248.84C292.54,-248.84 128.62,-248.84 128.62,-248.84 122.62,-248.84 116.62,-242.84 116.62,-236.84 116.62,-236.84 116.62,-206.56 116.62,-206.56 116.62,-200.56 122.62,-194.56 128.62,-194.56 128.62,-194.56 292.54,-194.56 292.54,-194.56 298.54,-194.56 304.54,-200.56 304.54,-206.56 304.54,-206.56 304.54,-236.84 304.54,-236.84 304.54,-242.84 298.54,-248.84 292.54,-248.84"/>
<text xml:space="preserve" text-anchor="start" x="167.08" y="-228.85" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#d97706">PARAMETER</text>
<text xml:space="preserve" text-anchor="start" x="129.58" y="-204.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Konstanten, Daten, Preise, Limits</text>
</g>
<!-- var -->
<g id="node2" class="node">
<title>var</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M314.29,-151.56C314.29,-151.56 106.88,-151.56 106.88,-151.56 100.88,-151.56 94.88,-145.56 94.88,-139.56 94.88,-139.56 94.88,-109.28 94.88,-109.28 94.88,-103.28 100.88,-97.28 106.88,-97.28 106.88,-97.28 314.29,-97.28 314.29,-97.28 320.29,-97.28 326.29,-103.28 326.29,-109.28 326.29,-109.28 326.29,-139.56 326.29,-139.56 326.29,-145.56 320.29,-151.56 314.29,-151.56"/>
<text xml:space="preserve" text-anchor="start" x="107.83" y="-131.57" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">ENTSCHEIDUNGSVARIABLEN</text>
<text xml:space="preserve" text-anchor="start" x="107.83" y="-107.42" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">x₁, x₂, …, xₙ : Was wird gesteuert?</text>
</g>
<!-- param&#45;&gt;var -->
<g id="edge1" class="edge">
<title>param&#45;&gt;var</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M210.58,-193.65C210.58,-184.6 210.58,-174.32 210.58,-164.53"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="213.74,-164.56 210.59,-155.56 207.44,-164.56 213.74,-164.56"/>
</g>
<!-- ziel -->
<g id="node3" class="node">
<title>ziel</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M181.17,-54.28C181.17,-54.28 12,-54.28 12,-54.28 6,-54.28 0,-48.28 0,-42.28 0,-42.28 0,-12 0,-12 0,-6 6,0 12,0 12,0 181.17,0 181.17,0 187.17,0 193.17,-6 193.17,-12 193.17,-12 193.17,-42.28 193.17,-42.28 193.17,-48.28 187.17,-54.28 181.17,-54.28"/>
<text xml:space="preserve" text-anchor="start" x="42.58" y="-34.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#059669">ZIELFUNKTION</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Minimiere Kosten / Maximiere ROI</text>
</g>
<!-- var&#45;&gt;ziel -->
<g id="edge2" class="edge">
<title>var&#45;&gt;ziel</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M178.22,-96.37C165.71,-85.91 151.23,-73.81 137.97,-62.73"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="140.28,-60.55 131.35,-57.2 136.24,-65.39 140.28,-60.55"/>
</g>
<!-- neben -->
<g id="node4" class="node">
<title>neben</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M412.17,-54.28C412.17,-54.28 237,-54.28 237,-54.28 231,-54.28 225,-48.28 225,-42.28 225,-42.28 225,-12 225,-12 225,-6 231,0 237,0 237,0 412.17,0 412.17,0 418.17,0 424.17,-6 424.17,-12 424.17,-12 424.17,-42.28 424.17,-42.28 424.17,-48.28 418.17,-54.28 412.17,-54.28"/>
<text xml:space="preserve" text-anchor="start" x="245.46" y="-34.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">NEBENBEDINGUNGEN</text>
<text xml:space="preserve" text-anchor="start" x="237.96" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Harte Schranken: Budget, Kapazität</text>
</g>
<!-- var&#45;&gt;neben -->
<g id="edge3" class="edge">
<title>var&#45;&gt;neben</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M242.95,-96.37C255.46,-85.91 269.94,-73.81 283.2,-62.73"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="284.93,-65.39 289.82,-57.2 280.89,-60.55 284.93,-65.39"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d05 Pages: 1 -->
<svg width="456pt" height="317pt"
viewBox="7.00 7.00 449.00 310.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 305.51)">
<title>d05</title>
<!-- modell -->
<g id="node1" class="node">
<title>modell</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M346.17,-294.31C346.17,-294.31 87.75,-294.31 87.75,-294.31 81.75,-294.31 75.75,-288.31 75.75,-282.31 75.75,-282.31 75.75,-242.03 75.75,-242.03 75.75,-236.03 81.75,-230.03 87.75,-230.03 87.75,-230.03 346.17,-230.03 346.17,-230.03 352.17,-230.03 358.17,-236.03 358.17,-242.03 358.17,-242.03 358.17,-282.31 358.17,-282.31 358.17,-288.31 352.17,-294.31 346.17,-294.31"/>
<text xml:space="preserve" text-anchor="start" x="88.71" y="-274.32" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">PYTHON&#45;MODELLIERUNGSSCHICHT</text>
<text xml:space="preserve" text-anchor="start" x="88.71" y="-250.17" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">scipy.optimize.linprog · Google OR&#45;Tools</text>
<text xml:space="preserve" text-anchor="start" x="88.71" y="-240.17" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">CVXPY · Pyomo / PuLP</text>
</g>
<!-- engines -->
<g id="node2" class="node">
<title>engines</title>
<polygon fill="#0891b2" stroke="none" points="12.96,-130.14 12.96,-164.64 420.96,-164.64 420.96,-130.14 12.96,-130.14"/>
<polygon fill="none" stroke="#0891b2" points="12.96,-130.14 12.96,-164.64 420.96,-164.64 420.96,-130.14 12.96,-130.14"/>
<text xml:space="preserve" text-anchor="start" x="118.34" y="-144.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#ffffff">SOLVER&#45;ENGINES (C / C++)</text>
<polygon fill="#ecfeff" stroke="none" points="12.96,-98.64 12.96,-130.14 161.46,-130.14 161.46,-98.64 12.96,-98.64"/>
<polygon fill="none" stroke="#0891b2" points="12.96,-98.64 12.96,-130.14 161.46,-130.14 161.46,-98.64 12.96,-98.64"/>
<text xml:space="preserve" text-anchor="start" x="47.46" y="-111.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#0891b2">LP · MIP · QP</text>
<polygon fill="#ecfeff" stroke="none" points="161.46,-98.64 161.46,-130.14 292.71,-130.14 292.71,-98.64 161.46,-98.64"/>
<polygon fill="none" stroke="#0891b2" points="161.46,-98.64 161.46,-130.14 292.71,-130.14 292.71,-98.64 161.46,-98.64"/>
<text xml:space="preserve" text-anchor="start" x="177.21" y="-111.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#0891b2">Constraint · SAT</text>
<polygon fill="#ecfeff" stroke="none" points="292.71,-98.64 292.71,-130.14 420.96,-130.14 420.96,-98.64 292.71,-98.64"/>
<polygon fill="none" stroke="#0891b2" points="292.71,-98.64 292.71,-130.14 420.96,-130.14 420.96,-98.64 292.71,-98.64"/>
<text xml:space="preserve" text-anchor="start" x="301.71" y="-111.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#0891b2">Konvex · QP · NLP</text>
<polygon fill="#ffffff" stroke="none" points="12.96,-53.64 12.96,-98.64 161.46,-98.64 161.46,-53.64 12.96,-53.64"/>
<polygon fill="none" stroke="#0891b2" points="12.96,-53.64 12.96,-98.64 161.46,-98.64 161.46,-53.64 12.96,-53.64"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-79.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">HiGHS</text>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-65.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">(Open Source / Default)</text>
<polygon fill="#ffffff" stroke="none" points="161.46,-53.64 161.46,-98.64 292.71,-98.64 292.71,-53.64 161.46,-53.64"/>
<polygon fill="none" stroke="#0891b2" points="161.46,-53.64 161.46,-98.64 292.71,-98.64 292.71,-53.64 161.46,-53.64"/>
<text xml:space="preserve" text-anchor="start" x="170.46" y="-79.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">CP&#45;SAT</text>
<text xml:space="preserve" text-anchor="start" x="170.46" y="-65.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">(Kombinatorik / SAT)</text>
<polygon fill="#ffffff" stroke="none" points="292.71,-53.64 292.71,-98.64 420.96,-98.64 420.96,-53.64 292.71,-53.64"/>
<polygon fill="none" stroke="#0891b2" points="292.71,-53.64 292.71,-98.64 420.96,-98.64 420.96,-53.64 292.71,-53.64"/>
<text xml:space="preserve" text-anchor="start" x="312.96" y="-79.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">OSQP / Clarabel</text>
<text xml:space="preserve" text-anchor="start" x="318.59" y="-65.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">(Konvex / QP)</text>
<polygon fill="#ffffff" stroke="none" points="12.96,-8.64 12.96,-53.64 161.46,-53.64 161.46,-8.64 12.96,-8.64"/>
<polygon fill="none" stroke="#0891b2" points="12.96,-8.64 12.96,-53.64 161.46,-53.64 161.46,-8.64 12.96,-8.64"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-34.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">Gurobi / CPLEX / Xpress</text>
<text xml:space="preserve" text-anchor="start" x="48.96" y="-20.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">(Kommerziell)</text>
<polygon fill="#ffffff" stroke="none" points="161.46,-8.64 161.46,-53.64 292.71,-53.64 292.71,-8.64 161.46,-8.64"/>
<polygon fill="none" stroke="#0891b2" points="161.46,-8.64 161.46,-53.64 292.71,-53.64 292.71,-8.64 161.46,-8.64"/>
<text xml:space="preserve" text-anchor="start" x="176.84" y="-34.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">SCIP / CBC / GLPK</text>
<text xml:space="preserve" text-anchor="start" x="176.84" y="-20.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">(OSS akademisch)</text>
<polygon fill="#ffffff" stroke="none" points="292.71,-8.64 292.71,-53.64 420.96,-53.64 420.96,-8.64 292.71,-8.64"/>
<polygon fill="none" stroke="#0891b2" points="292.71,-8.64 292.71,-53.64 420.96,-53.64 420.96,-8.64 292.71,-8.64"/>
<text xml:space="preserve" text-anchor="start" x="307.34" y="-34.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">IPOPT</text>
<text xml:space="preserve" text-anchor="start" x="307.34" y="-20.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">(NLP / nichtlinear)</text>
</g>
<!-- modell&#45;&gt;engines -->
<g id="edge1" class="edge">
<title>modell&#45;&gt;engines</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M216.96,-229.34C216.96,-216.51 216.96,-200.95 216.96,-184.91"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="220.11,-185.2 216.96,-176.2 213.81,-185.2 220.11,-185.2"/>
<text xml:space="preserve" text-anchor="middle" x="282.96" y="-198.53" font-family="DejaVu Sans" font-size="10.00" fill="#334155"> &#160;C&#45;API / FFI / Matrix&#45;Export</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d22 Pages: 1 -->
<svg width="359pt" height="248pt"
viewBox="7.00 7.00 352.00 241.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 236.51)">
<title>d22</title>
<!-- primal -->
<g id="node1" class="node">
<title>primal</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M302.3,-225.31C302.3,-225.31 34.12,-225.31 34.12,-225.31 28.12,-225.31 22.12,-219.31 22.12,-213.31 22.12,-213.31 22.12,-153.03 22.12,-153.03 22.12,-147.03 28.12,-141.03 34.12,-141.03 34.12,-141.03 302.3,-141.03 302.3,-141.03 308.3,-141.03 314.3,-147.03 314.3,-153.03 314.3,-153.03 314.3,-213.31 314.3,-213.31 314.3,-219.31 308.3,-225.31 302.3,-225.31"/>
<text xml:space="preserve" text-anchor="start" x="35.09" y="-205.32" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">PRIMALES PROBLEM &#160;(Unternehmer)</text>
<text xml:space="preserve" text-anchor="start" x="35.09" y="-181.17" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Maximiere Gewinn aus Produkten</text>
<text xml:space="preserve" text-anchor="start" x="35.09" y="-171.17" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Zielfunktion: &#160;&#160;max &#160;cᵀx</text>
<text xml:space="preserve" text-anchor="start" x="35.09" y="-161.17" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Restriktionen: &#160;A x ≤ b</text>
<text xml:space="preserve" text-anchor="start" x="35.09" y="-151.17" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Variablen: &#160;x ≥ 0 &#160;(Produktionsmengen)</text>
</g>
<!-- dual -->
<g id="node2" class="node">
<title>dual</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M324.42,-84.28C324.42,-84.28 12,-84.28 12,-84.28 6,-84.28 0,-78.28 0,-72.28 0,-72.28 0,-12 0,-12 0,-6 6,0 12,0 12,0 324.42,0 324.42,0 330.42,0 336.42,-6 336.42,-12 336.42,-12 336.42,-72.28 336.42,-72.28 336.42,-78.28 330.42,-84.28 324.42,-84.28"/>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-64.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">DUALES PROBLEM &#160;(Ressourcen&#45;Bewerter)</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-40.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Minimiere Bewertung der Ressourcen</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-30.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Zielfunktion: &#160;&#160;min &#160;bᵀy</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Restriktionen: &#160;Aᵀy ≥ c</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Variablen: &#160;y ≥ 0 &#160;(Schattenpreise)</text>
</g>
<!-- primal&#45;&gt;dual -->
<g id="edge1" class="edge">
<title>primal&#45;&gt;dual</title>
<path fill="none" stroke="#7c3aed" stroke-width="2.4" d="M168.21,-127.45C168.21,-117.65 168.21,-107.44 168.21,-97.65"/>
<polygon fill="#7c3aed" stroke="#7c3aed" stroke-width="2.4" points="165.06,-127.45 168.21,-136.45 171.36,-127.45 165.06,-127.45"/>
<polygon fill="#7c3aed" stroke="#7c3aed" stroke-width="2.4" points="171.36,-97.72 168.21,-88.72 165.06,-97.72 171.36,-97.72"/>
<text xml:space="preserve" text-anchor="middle" x="193.71" y="-109.53" font-family="DejaVu Sans" font-size="10.00" fill="#7c3aed"> &#160;Dualität &#160;</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d09 Pages: 1 -->
<svg width="393pt" height="321pt"
viewBox="7.00 7.00 386.00 314.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 310.04)">
<title>d09</title>
<!-- store -->
<g id="node1" class="node">
<title>store</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M290.3,-298.84C290.3,-298.84 80.63,-298.84 80.63,-298.84 74.63,-298.84 68.63,-292.84 68.63,-286.84 68.63,-286.84 68.63,-246.56 68.63,-246.56 68.63,-240.56 74.63,-234.56 80.63,-234.56 80.63,-234.56 290.3,-234.56 290.3,-234.56 296.3,-234.56 302.3,-240.56 302.3,-246.56 302.3,-246.56 302.3,-286.84 302.3,-286.84 302.3,-292.84 296.3,-298.84 290.3,-298.84"/>
<text xml:space="preserve" text-anchor="start" x="111.96" y="-278.85" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">CONSTRAINT STORE</text>
<text xml:space="preserve" text-anchor="start" x="81.59" y="-254.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Domain x₁ ∈ {1, 2, 3, 4}, &#160;x₂ ∈ {2, 3, 4, 5}</text>
<text xml:space="preserve" text-anchor="start" x="81.59" y="-244.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Constraint: x₁ + x₂ = 7</text>
</g>
<!-- prop -->
<g id="node2" class="node">
<title>prop</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M331.92,-191.56C331.92,-191.56 39,-191.56 39,-191.56 33,-191.56 27,-185.56 27,-179.56 27,-179.56 27,-129.28 27,-129.28 27,-123.28 33,-117.28 39,-117.28 39,-117.28 331.92,-117.28 331.92,-117.28 337.92,-117.28 343.92,-123.28 343.92,-129.28 343.92,-129.28 343.92,-179.56 343.92,-179.56 343.92,-185.56 337.92,-191.56 331.92,-191.56"/>
<text xml:space="preserve" text-anchor="start" x="39.96" y="-171.57" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">CONSTRAINT PROPAGATION &#160;(Filterung)</text>
<text xml:space="preserve" text-anchor="start" x="39.96" y="-147.42" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">• x₁ = 1 ⇒ x₂ = 6 &#160;(nicht in Domain!)</text>
<text xml:space="preserve" text-anchor="start" x="39.96" y="-137.42" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e"> &#160;⟶ entferne 1 aus Domain(x₁)</text>
<text xml:space="preserve" text-anchor="start" x="39.96" y="-127.42" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">• Reduziert: x₁ ∈ {2, 3, 4}, x₂ ∈ {3, 4, 5}</text>
</g>
<!-- store&#45;&gt;prop -->
<g id="edge1" class="edge">
<title>store&#45;&gt;prop</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M185.46,-233.8C185.46,-224.58 185.46,-214.29 185.46,-204.28"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="188.61,-204.33 185.46,-195.33 182.31,-204.33 188.61,-204.33"/>
</g>
<!-- sat -->
<g id="node3" class="node">
<title>sat</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M358.92,-74.28C358.92,-74.28 12,-74.28 12,-74.28 6,-74.28 0,-68.28 0,-62.28 0,-62.28 0,-12 0,-12 0,-6 6,0 12,0 12,0 358.92,0 358.92,0 364.92,0 370.92,-6 370.92,-12 370.92,-12 370.92,-62.28 370.92,-62.28 370.92,-68.28 364.92,-74.28 358.92,-74.28"/>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-54.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#7c3aed">SAT&#45;SEARCH &amp; CONFLICT LEARNING &#160;(Nogoods)</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-30.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">• Bei Widerspruch lernt der SAT&#45;Core eine Klausel</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e"> &#160;(„diese Belegung nie wieder prüfen“)</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">• Non&#45;chronological Backtracking</text>
</g>
<!-- prop&#45;&gt;sat -->
<g id="edge2" class="edge">
<title>prop&#45;&gt;sat</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M185.46,-116.31C185.46,-106.95 185.46,-96.76 185.46,-86.92"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="188.61,-87.21 185.46,-78.21 182.31,-87.21 188.61,-87.21"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d13 Pages: 1 -->
<svg width="568pt" height="178pt"
viewBox="7.00 7.00 561.00 170.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 166.48)">
<title>d13</title>
<!-- t -->
<g id="node1" class="node">
<title>t</title>
<polygon fill="#7c3aed" stroke="none" points="12.96,-112.14 12.96,-146.64 532.71,-146.64 532.71,-112.14 12.96,-112.14"/>
<polygon fill="none" stroke="#7c3aed" points="12.96,-112.14 12.96,-146.64 532.71,-146.64 532.71,-112.14 12.96,-112.14"/>
<text xml:space="preserve" text-anchor="start" x="132.96" y="-126.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#ffffff">ANSÄTZE FÜR UNBEKANNTE ZUKUNFT</text>
<polygon fill="#eef2ff" stroke="none" points="12.96,-80.64 12.96,-112.14 168.21,-112.14 168.21,-80.64 12.96,-80.64"/>
<polygon fill="none" stroke="#7c3aed" points="12.96,-80.64 12.96,-112.14 168.21,-112.14 168.21,-80.64 12.96,-80.64"/>
<text xml:space="preserve" text-anchor="start" x="46.33" y="-93.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#4338ca">MONTE CARLO</text>
<polygon fill="#ecfeff" stroke="none" points="168.21,-80.64 168.21,-112.14 366.96,-112.14 366.96,-80.64 168.21,-80.64"/>
<polygon fill="none" stroke="#7c3aed" points="168.21,-80.64 168.21,-112.14 366.96,-112.14 366.96,-80.64 168.21,-80.64"/>
<text xml:space="preserve" text-anchor="start" x="177.21" y="-93.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#0891b2">STOCHASTISCHES RECOURSE</text>
<polygon fill="#ecfdf5" stroke="none" points="366.96,-80.64 366.96,-112.14 532.71,-112.14 532.71,-80.64 366.96,-80.64"/>
<polygon fill="none" stroke="#7c3aed" points="366.96,-80.64 366.96,-112.14 532.71,-112.14 532.71,-80.64 366.96,-80.64"/>
<text xml:space="preserve" text-anchor="start" x="375.96" y="-93.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#059669">ROBUSTE OPTIMIERUNG</text>
<polygon fill="#ffffff" stroke="none" points="12.96,-8.64 12.96,-80.64 168.21,-80.64 168.21,-8.64 12.96,-8.64"/>
<polygon fill="none" stroke="#7c3aed" points="12.96,-8.64 12.96,-80.64 168.21,-80.64 168.21,-8.64 12.96,-8.64"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-61.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">• Simuliert Szenarien</text>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-47.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">• Misst Verlustverteilung</text>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-34.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">• Reine Evaluierung,</text>
<text xml:space="preserve" text-anchor="start" x="36.21" y="-20.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e"> &#160;keine Optimierung</text>
<polygon fill="#ffffff" stroke="none" points="168.21,-8.64 168.21,-80.64 366.96,-80.64 366.96,-8.64 168.21,-8.64"/>
<polygon fill="none" stroke="#7c3aed" points="168.21,-8.64 168.21,-80.64 366.96,-80.64 366.96,-8.64 168.21,-8.64"/>
<text xml:space="preserve" text-anchor="start" x="177.21" y="-61.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">• Entscheidet Stufe 1</text>
<text xml:space="preserve" text-anchor="start" x="177.21" y="-47.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">• Passt in Stufe 2 an</text>
<text xml:space="preserve" text-anchor="start" x="177.21" y="-34.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">• Optimiert Erwartungswert</text>
<text xml:space="preserve" text-anchor="start" x="197.46" y="-20.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e"> &#160;über alle Szenarien</text>
<polygon fill="#ffffff" stroke="none" points="366.96,-8.64 366.96,-80.64 532.71,-80.64 532.71,-8.64 366.96,-8.64"/>
<polygon fill="none" stroke="#7c3aed" points="366.96,-8.64 366.96,-80.64 532.71,-80.64 532.71,-8.64 366.96,-8.64"/>
<text xml:space="preserve" text-anchor="start" x="375.96" y="-61.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">• Sucht den Worst&#45;Case</text>
<text xml:space="preserve" text-anchor="start" x="375.96" y="-47.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">• Keine Verteilungs&#45;</text>
<text xml:space="preserve" text-anchor="start" x="396.96" y="-34.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e"> &#160;annahme nötig</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d14 Pages: 1 -->
<svg width="543pt" height="97pt"
viewBox="7.00 7.00 536.00 89.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 85.48)">
<title>d14</title>
<!-- S0 -->
<g id="node1" class="node">
<title>S0</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M92.67,-74.28C92.67,-74.28 12,-74.28 12,-74.28 6,-74.28 0,-68.28 0,-62.28 0,-62.28 0,-12 0,-12 0,-6 6,0 12,0 12,0 92.67,0 92.67,0 98.67,0 104.67,-6 104.67,-12 104.67,-12 104.67,-62.28 104.67,-62.28 104.67,-68.28 98.67,-74.28 92.67,-74.28"/>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-54.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">Zustand S₀</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-30.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">t = 0</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Aktion a₀</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Kosten C₀</text>
</g>
<!-- S1 -->
<g id="node2" class="node">
<title>S1</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M240.34,-74.28C240.34,-74.28 159.67,-74.28 159.67,-74.28 153.67,-74.28 147.67,-68.28 147.67,-62.28 147.67,-62.28 147.67,-12 147.67,-12 147.67,-6 153.67,0 159.67,0 159.67,0 240.34,0 240.34,0 246.34,0 252.34,-6 252.34,-12 252.34,-12 252.34,-62.28 252.34,-62.28 252.34,-68.28 246.34,-74.28 240.34,-74.28"/>
<text xml:space="preserve" text-anchor="start" x="160.63" y="-54.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">Zustand S₁</text>
<text xml:space="preserve" text-anchor="start" x="160.63" y="-30.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">t = 1</text>
<text xml:space="preserve" text-anchor="start" x="160.63" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Aktion a₁</text>
<text xml:space="preserve" text-anchor="start" x="160.63" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Kosten C₁</text>
</g>
<!-- S0&#45;&gt;S1 -->
<g id="edge1" class="edge">
<title>S0&#45;&gt;S1</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M105.51,-37.14C115.03,-37.14 125.06,-37.14 134.88,-37.14"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="134.67,-40.29 143.67,-37.14 134.67,-33.99 134.67,-40.29"/>
</g>
<!-- dots -->
<g id="node3" class="node">
<title>dots</title>
<text xml:space="preserve" text-anchor="middle" x="322.34" y="-30.14" font-family="DejaVu Sans" font-size="20.00" fill="#334155"></text>
</g>
<!-- S1&#45;&gt;dots -->
<g id="edge2" class="edge">
<title>S1&#45;&gt;dots</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M253.09,-37.14C263.45,-37.14 274.13,-37.14 283.89,-37.14"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="283.62,-40.29 292.62,-37.14 283.62,-33.99 283.62,-40.29"/>
</g>
<!-- ST -->
<g id="node4" class="node">
<title>ST</title>
<path fill="#059669" stroke="#059669" stroke-width="2" d="M509.01,-69.28C509.01,-69.28 404.34,-69.28 404.34,-69.28 398.34,-69.28 392.34,-63.28 392.34,-57.28 392.34,-57.28 392.34,-17 392.34,-17 392.34,-11 398.34,-5 404.34,-5 404.34,-5 509.01,-5 509.01,-5 515.01,-5 521.01,-11 521.01,-17 521.01,-17 521.01,-57.28 521.01,-57.28 521.01,-63.28 515.01,-69.28 509.01,-69.28"/>
<text xml:space="preserve" text-anchor="start" x="405.3" y="-49.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#ffffff">Endzustand Sₜ</text>
<text xml:space="preserve" text-anchor="start" x="405.3" y="-25.14" font-family="DejaVu Sans" font-size="10.00" fill="#ffffff">t = T</text>
<text xml:space="preserve" text-anchor="start" x="405.3" y="-15.14" font-family="DejaVu Sans" font-size="10.00" fill="#ffffff">alles verkauft</text>
</g>
<!-- dots&#45;&gt;ST -->
<g id="edge3" class="edge">
<title>dots&#45;&gt;ST</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M349.17,-37.14C358.12,-37.14 368.66,-37.14 379.57,-37.14"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="379.47,-40.29 388.47,-37.14 379.47,-33.99 379.47,-40.29"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d15 Pages: 1 -->
<svg width="425pt" height="301pt"
viewBox="7.00 7.00 417.00 294.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 290.04)">
<title>d15</title>
<!-- S0 -->
<g id="node1" class="node">
<title>S0</title>
<path fill="#fff1f3" stroke="#e11d48" stroke-width="2" d="M302.29,-278.84C302.29,-278.84 88.88,-278.84 88.88,-278.84 82.88,-278.84 76.87,-272.84 76.87,-266.84 76.87,-266.84 76.87,-206.56 76.87,-206.56 76.87,-200.56 82.87,-194.56 88.87,-194.56 88.87,-194.56 302.29,-194.56 302.29,-194.56 308.29,-194.56 314.29,-200.56 314.29,-206.56 314.29,-206.56 314.29,-266.84 314.29,-266.84 314.29,-272.84 308.29,-278.84 302.29,-278.84"/>
<text xml:space="preserve" text-anchor="start" x="89.83" y="-258.85" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#e11d48">STICHPROBEN&#45;KOVARIANZ &#160;S</text>
<text xml:space="preserve" text-anchor="start" x="89.83" y="-234.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">• T &lt; N: singulär (Rang &lt; N)</text>
<text xml:space="preserve" text-anchor="start" x="89.83" y="-224.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">• T ≈ N: instabil, Eigenwerte streuen</text>
<text xml:space="preserve" text-anchor="start" x="89.83" y="-214.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">• kleine Eigenwerte unterschätzt</text>
<text xml:space="preserve" text-anchor="start" x="89.83" y="-204.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e"> &#160;⟶ Solver nutzt Scheinkorrelationen</text>
</g>
<!-- LW -->
<g id="node2" class="node">
<title>LW</title>
<path fill="#059669" stroke="#059669" stroke-width="2" d="M315.79,-151.56C315.79,-151.56 75.38,-151.56 75.38,-151.56 69.38,-151.56 63.37,-145.56 63.37,-139.56 63.37,-139.56 63.37,-109.28 63.37,-109.28 63.37,-103.28 69.37,-97.28 75.37,-97.28 75.37,-97.28 315.79,-97.28 315.79,-97.28 321.79,-97.28 327.79,-103.28 327.79,-109.28 327.79,-109.28 327.79,-139.56 327.79,-139.56 327.79,-145.56 321.79,-151.56 315.79,-151.56"/>
<text xml:space="preserve" text-anchor="start" x="76.33" y="-131.57" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#ffffff">LEDOIT&#45;WOLF SHRINKAGE &#160;Σ(LW)</text>
<text xml:space="preserve" text-anchor="start" x="76.33" y="-107.42" font-family="DejaVu Sans" font-size="10.00" fill="#ffffff">Σ(LW) = (1 δ) · S &#160;+ &#160;δ · F</text>
</g>
<!-- S0&#45;&gt;LW -->
<g id="edge1" class="edge">
<title>S0&#45;&gt;LW</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M195.58,-193.69C195.58,-184.04 195.58,-173.82 195.58,-164.31"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="198.74,-164.35 195.59,-155.35 192.44,-164.35 198.74,-164.35"/>
</g>
<!-- S -->
<g id="node3" class="node">
<title>S</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M163.17,-54.28C163.17,-54.28 12,-54.28 12,-54.28 6,-54.28 0,-48.28 0,-42.28 0,-42.28 0,-12 0,-12 0,-6 6,0 12,0 12,0 163.17,0 163.17,0 169.17,0 175.17,-6 175.17,-12 175.17,-12 175.17,-42.28 175.17,-42.28 175.17,-48.28 169.17,-54.28 163.17,-54.28"/>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-34.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">Stichprobenmatrix S</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">unverzerrt, hohes Rauschen</text>
</g>
<!-- LW&#45;&gt;S -->
<g id="edge2" class="edge">
<title>LW&#45;&gt;S</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M164.93,-96.37C153.18,-86.01 139.62,-74.04 127.16,-63.05"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="129.42,-60.85 120.59,-57.26 125.26,-65.58 129.42,-60.85"/>
</g>
<!-- F -->
<g id="node4" class="node">
<title>F</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M390.29,-54.28C390.29,-54.28 218.88,-54.28 218.88,-54.28 212.88,-54.28 206.88,-48.28 206.88,-42.28 206.88,-42.28 206.88,-12 206.88,-12 206.88,-6 212.88,0 218.88,0 218.88,0 390.29,0 390.29,0 396.29,0 402.29,-6 402.29,-12 402.29,-12 402.29,-42.28 402.29,-42.28 402.29,-48.28 396.29,-54.28 390.29,-54.28"/>
<text xml:space="preserve" text-anchor="start" x="219.83" y="-34.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">Strukturiertes Target F</text>
<text xml:space="preserve" text-anchor="start" x="219.83" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">verzerrt, kein Rauschen</text>
</g>
<!-- LW&#45;&gt;F -->
<g id="edge3" class="edge">
<title>LW&#45;&gt;F</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M226.53,-96.37C238.38,-86.01 252.07,-74.04 264.65,-63.05"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="266.58,-65.54 271.29,-57.25 262.44,-60.8 266.58,-65.54"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d19 Pages: 1 -->
<svg width="503pt" height="516pt"
viewBox="7.00 7.00 495.00 509.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 504.6)">
<title>d19</title>
<!-- s1 -->
<g id="node1" class="node">
<title>s1</title>
<path fill="#fffbeb" stroke="#d97706" stroke-width="2" d="M370.67,-493.4C370.67,-493.4 128,-493.4 128,-493.4 122,-493.4 116,-487.4 116,-481.4 116,-481.4 116,-441.12 116,-441.12 116,-435.12 122,-429.12 128,-429.12 128,-429.12 370.67,-429.12 370.67,-429.12 376.67,-429.12 382.67,-435.12 382.67,-441.12 382.67,-441.12 382.67,-481.4 382.67,-481.4 382.67,-487.4 376.67,-493.4 370.67,-493.4"/>
<text xml:space="preserve" text-anchor="start" x="128.96" y="-473.41" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#d97706">1 · HISTORISCHE DATENPIPELINE</text>
<text xml:space="preserve" text-anchor="start" x="128.96" y="-449.26" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Automatischer Download bereinigter</text>
<text xml:space="preserve" text-anchor="start" x="128.96" y="-439.26" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Kursdaten über 5 Jahre</text>
</g>
<!-- s2 -->
<g id="node2" class="node">
<title>s2</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M230.67,-386.12C230.67,-386.12 12,-386.12 12,-386.12 6,-386.12 0,-380.12 0,-374.12 0,-374.12 0,-333.84 0,-333.84 0,-327.84 6,-321.84 12,-321.84 12,-321.84 230.67,-321.84 230.67,-321.84 236.67,-321.84 242.67,-327.84 242.67,-333.84 242.67,-333.84 242.67,-374.12 242.67,-374.12 242.67,-380.12 236.67,-386.12 230.67,-386.12"/>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-366.13" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">2 · FAKTOR&#45;SCORING&#45;MODELL</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-341.98" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">121M Momentum</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-331.98" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">als Rendite&#45;Signal μₜ</text>
</g>
<!-- s1&#45;&gt;s2 -->
<g id="edge1" class="edge">
<title>s1&#45;&gt;s2</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M210.58,-428.39C197.67,-417.77 183.12,-405.8 169.58,-394.66"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="171.69,-392.32 162.73,-389.03 167.68,-397.18 171.69,-392.32"/>
</g>
<!-- s3 -->
<g id="node3" class="node">
<title>s3</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M468.29,-386.12C468.29,-386.12 286.38,-386.12 286.38,-386.12 280.38,-386.12 274.38,-380.12 274.38,-374.12 274.38,-374.12 274.38,-333.84 274.38,-333.84 274.38,-327.84 280.38,-321.84 286.38,-321.84 286.38,-321.84 468.29,-321.84 468.29,-321.84 474.29,-321.84 480.29,-327.84 480.29,-333.84 480.29,-333.84 480.29,-374.12 480.29,-374.12 480.29,-380.12 474.29,-386.12 468.29,-386.12"/>
<text xml:space="preserve" text-anchor="start" x="287.33" y="-366.13" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">3 · ROBUSTE KOVARIANZ</text>
<text xml:space="preserve" text-anchor="start" x="287.33" y="-341.98" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Ledoit&#45;Wolf&#45;Shrinkage auf</text>
<text xml:space="preserve" text-anchor="start" x="287.33" y="-331.98" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">rollierendem 252&#45;Tage&#45;Fenster</text>
</g>
<!-- s1&#45;&gt;s3 -->
<g id="edge2" class="edge">
<title>s1&#45;&gt;s3</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M288.09,-428.39C301,-417.77 315.55,-405.8 329.09,-394.66"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="330.99,-397.18 335.94,-389.03 326.98,-392.32 330.99,-397.18"/>
</g>
<!-- s4 -->
<g id="node4" class="node">
<title>s4</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M383.42,-278.84C383.42,-278.84 115.25,-278.84 115.25,-278.84 109.25,-278.84 103.25,-272.84 103.25,-266.84 103.25,-266.84 103.25,-226.56 103.25,-226.56 103.25,-220.56 109.25,-214.56 115.25,-214.56 115.25,-214.56 383.42,-214.56 383.42,-214.56 389.42,-214.56 395.42,-220.56 395.42,-226.56 395.42,-226.56 395.42,-266.84 395.42,-266.84 395.42,-272.84 389.42,-278.84 383.42,-278.84"/>
<text xml:space="preserve" text-anchor="start" x="116.21" y="-258.85" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#7c3aed">4 · OR&#45;OPTIMIERUNGSKERN (CVXPY)</text>
<text xml:space="preserve" text-anchor="start" x="116.21" y="-234.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Max. Momentum CVaR Reibung</text>
<text xml:space="preserve" text-anchor="start" x="116.21" y="-224.7" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Hard Constraints: Sektor&#45;Caps, No&#45;Short</text>
</g>
<!-- s2&#45;&gt;s4 -->
<g id="edge3" class="edge">
<title>s2&#45;&gt;s4</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M160.09,-321.11C173,-310.49 187.55,-298.52 201.09,-287.38"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="202.99,-289.9 207.94,-281.75 198.98,-285.04 202.99,-289.9"/>
</g>
<!-- s3&#45;&gt;s4 -->
<g id="edge4" class="edge">
<title>s3&#45;&gt;s4</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M338.58,-321.11C325.67,-310.49 311.12,-298.52 297.58,-287.38"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="299.69,-285.04 290.73,-281.75 295.68,-289.9 299.69,-285.04"/>
</g>
<!-- s5 -->
<g id="node5" class="node">
<title>s5</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M393.54,-171.56C393.54,-171.56 105.12,-171.56 105.12,-171.56 99.12,-171.56 93.12,-165.56 93.12,-159.56 93.12,-159.56 93.12,-119.28 93.12,-119.28 93.12,-113.28 99.12,-107.28 105.12,-107.28 105.12,-107.28 393.54,-107.28 393.54,-107.28 399.54,-107.28 405.54,-113.28 405.54,-119.28 405.54,-119.28 405.54,-159.56 405.54,-159.56 405.54,-165.56 399.54,-171.56 393.54,-171.56"/>
<text xml:space="preserve" text-anchor="start" x="106.08" y="-151.57" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">5 · REBALANCING &amp; ORDER&#45;EXECUTION</text>
<text xml:space="preserve" text-anchor="start" x="106.08" y="-127.42" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Monatliche Neugewichtung mit</text>
<text xml:space="preserve" text-anchor="start" x="106.08" y="-117.42" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">realer Transaktionskosten&#45;Verbuchung</text>
</g>
<!-- s4&#45;&gt;s5 -->
<g id="edge5" class="edge">
<title>s4&#45;&gt;s5</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M249.33,-213.83C249.33,-204.53 249.33,-194.21 249.33,-184.31"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="252.49,-184.58 249.34,-175.58 246.19,-184.58 252.49,-184.58"/>
</g>
<!-- s6 -->
<g id="node6" class="node">
<title>s6</title>
<path fill="#059669" stroke="#059669" stroke-width="2" d="M388.29,-64.28C388.29,-64.28 110.38,-64.28 110.38,-64.28 104.38,-64.28 98.37,-58.28 98.37,-52.28 98.37,-52.28 98.37,-12 98.37,-12 98.37,-6 104.37,0 110.37,0 110.37,0 388.29,0 388.29,0 394.29,0 400.29,-6 400.29,-12 400.29,-12 400.29,-52.28 400.29,-52.28 400.29,-58.28 394.29,-64.28 388.29,-64.28"/>
<text xml:space="preserve" text-anchor="start" x="111.33" y="-44.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#ffffff">6 · LEISTUNGSREPORT &amp; BENCHMARK</text>
<text xml:space="preserve" text-anchor="start" x="111.33" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#ffffff">Sharpe, Max Drawdown, CAGR</text>
<text xml:space="preserve" text-anchor="start" x="111.33" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#ffffff">vs. S&amp;P 500 (SPY)</text>
</g>
<!-- s5&#45;&gt;s6 -->
<g id="edge6" class="edge">
<title>s5&#45;&gt;s6</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M249.33,-106.55C249.33,-97.25 249.33,-86.93 249.33,-77.03"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="252.49,-77.3 249.34,-68.3 246.19,-77.3 252.49,-77.3"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d18 Pages: 1 -->
<svg width="350pt" height="581pt"
viewBox="7.00 7.00 343.00 574.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 569.6)">
<title>d18</title>
<!-- s0 -->
<g id="node1" class="node">
<title>s0</title>
<path fill="#fffbeb" stroke="#d97706" stroke-width="2" d="M225.42,-558.4C225.42,-558.4 69.75,-558.4 69.75,-558.4 63.75,-558.4 57.75,-552.4 57.75,-546.4 57.75,-546.4 57.75,-506.12 57.75,-506.12 57.75,-500.12 63.75,-494.12 69.75,-494.12 69.75,-494.12 225.42,-494.12 225.42,-494.12 231.42,-494.12 237.42,-500.12 237.42,-506.12 237.42,-506.12 237.42,-546.4 237.42,-546.4 237.42,-552.4 231.42,-558.4 225.42,-558.4"/>
<text xml:space="preserve" text-anchor="start" x="87.59" y="-538.41" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#d97706">QUELL&#45;SYSTEME</text>
<text xml:space="preserve" text-anchor="start" x="70.71" y="-514.26" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">ERP / CRM / HR&#45;Kalender</text>
<text xml:space="preserve" text-anchor="start" x="70.71" y="-504.26" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Marktdaten&#45;Feeds / Broker&#45;APIs</text>
</g>
<!-- s1 -->
<g id="node2" class="node">
<title>s1</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M255.42,-437.37C255.42,-437.37 39.75,-437.37 39.75,-437.37 33.75,-437.37 27.75,-431.37 27.75,-425.37 27.75,-425.37 27.75,-385.09 27.75,-385.09 27.75,-379.09 33.75,-373.09 39.75,-373.09 39.75,-373.09 255.42,-373.09 255.42,-373.09 261.42,-373.09 267.42,-379.09 267.42,-385.09 267.42,-385.09 267.42,-425.37 267.42,-425.37 267.42,-431.37 261.42,-437.37 255.42,-437.37"/>
<text xml:space="preserve" text-anchor="start" x="40.71" y="-417.38" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#0891b2">DATA QUALITY &amp; SNAPSHOTS</text>
<text xml:space="preserve" text-anchor="start" x="40.71" y="-393.23" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Validierung (Pydantic / Pandera)</text>
<text xml:space="preserve" text-anchor="start" x="40.71" y="-383.23" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Unveränderlicher Planungs&#45;Snapshot</text>
</g>
<!-- s0&#45;&gt;s1 -->
<g id="edge1" class="edge">
<title>s0&#45;&gt;s1</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M147.59,-493.36C147.59,-480.02 147.59,-464.3 147.59,-449.91"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="150.74,-450.24 147.59,-441.24 144.44,-450.24 150.74,-450.24"/>
<text xml:space="preserve" text-anchor="middle" x="204.96" y="-462.62" font-family="DejaVu Sans" font-size="10.00" fill="#334155"> &#160;Events / REST / Polling</text>
</g>
<!-- s2 -->
<g id="node3" class="node">
<title>s2</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M267.42,-316.34C267.42,-316.34 27.75,-316.34 27.75,-316.34 21.75,-316.34 15.75,-310.34 15.75,-304.34 15.75,-304.34 15.75,-254.06 15.75,-254.06 15.75,-248.06 21.75,-242.06 27.75,-242.06 27.75,-242.06 267.42,-242.06 267.42,-242.06 273.42,-242.06 279.42,-248.06 279.42,-254.06 279.42,-254.06 279.42,-304.34 279.42,-304.34 279.42,-310.34 273.42,-316.34 267.42,-316.34"/>
<text xml:space="preserve" text-anchor="start" x="28.71" y="-296.35" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#4338ca">OPTIMIERUNGS&#45;WORKER (async)</text>
<text xml:space="preserve" text-anchor="start" x="28.71" y="-272.2" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Queue (Celery / Redis / Dramatiq)</text>
<text xml:space="preserve" text-anchor="start" x="28.71" y="-262.2" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Solver: OR&#45;Tools / HiGHS / CVXPY</text>
<text xml:space="preserve" text-anchor="start" x="28.71" y="-252.2" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Timeouts, Fallbacks, Diagnose</text>
</g>
<!-- s1&#45;&gt;s2 -->
<g id="edge2" class="edge">
<title>s1&#45;&gt;s2</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M147.59,-372.29C147.59,-359.17 147.59,-343.68 147.59,-329.18"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="150.74,-329.34 147.59,-320.34 144.44,-329.34 150.74,-329.34"/>
<text xml:space="preserve" text-anchor="middle" x="183.59" y="-341.59" font-family="DejaVu Sans" font-size="10.00" fill="#334155"> &#160;JSON&#45;Payload</text>
</g>
<!-- s3 -->
<g id="node4" class="node">
<title>s3</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M283.17,-185.31C283.17,-185.31 12,-185.31 12,-185.31 6,-185.31 0,-179.31 0,-173.31 0,-173.31 0,-133.03 0,-133.03 0,-127.03 6,-121.03 12,-121.03 12,-121.03 283.17,-121.03 283.17,-121.03 289.17,-121.03 295.17,-127.03 295.17,-133.03 295.17,-133.03 295.17,-173.31 295.17,-173.31 295.17,-179.31 289.17,-185.31 283.17,-185.31"/>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-165.32" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#7c3aed">DISPOSITION &amp; HUMAN&#45;IN&#45;THE&#45;LOOP</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-141.17" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Web&#45;UI / Dashboard</text>
<text xml:space="preserve" text-anchor="start" x="12.96" y="-131.17" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Freigabeprozess (Mensch bestätigt)</text>
</g>
<!-- s2&#45;&gt;s3 -->
<g id="edge3" class="edge">
<title>s2&#45;&gt;s3</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M147.59,-241.32C147.59,-227.74 147.59,-212.21 147.59,-198.07"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="150.74,-198.1 147.59,-189.1 144.44,-198.1 150.74,-198.1"/>
<text xml:space="preserve" text-anchor="middle" x="237.59" y="-210.56" font-family="DejaVu Sans" font-size="10.00" fill="#334155"> &#160;Ergebnis + Kostenzerlegung + Logs</text>
</g>
<!-- s4 -->
<g id="node5" class="node">
<title>s4</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M243.79,-64.28C243.79,-64.28 51.38,-64.28 51.38,-64.28 45.38,-64.28 39.38,-58.28 39.38,-52.28 39.38,-52.28 39.38,-12 39.38,-12 39.38,-6 45.38,0 51.38,0 51.38,0 243.79,0 243.79,0 249.79,0 255.79,-6 255.79,-12 255.79,-12 255.79,-52.28 255.79,-52.28 255.79,-58.28 249.79,-64.28 243.79,-64.28"/>
<text xml:space="preserve" text-anchor="start" x="59.84" y="-44.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#059669">AUSFÜHRUNGSSCHICHT</text>
<text xml:space="preserve" text-anchor="start" x="52.34" y="-20.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Kalender&#45;Sync / Push&#45;Benachrichtigung</text>
<text xml:space="preserve" text-anchor="start" x="52.34" y="-10.14" font-family="DejaVu Sans" font-size="10.00" fill="#1a1a2e">Broker&#45;Order&#45;Routing</text>
</g>
<!-- s3&#45;&gt;s4 -->
<g id="edge4" class="edge">
<title>s3&#45;&gt;s4</title>
<path fill="none" stroke="#334155" stroke-width="2" d="M147.59,-120.27C147.59,-106.93 147.59,-91.21 147.59,-76.82"/>
<polygon fill="#334155" stroke="#334155" stroke-width="2" points="150.74,-77.15 147.59,-68.15 144.44,-77.15 150.74,-77.15"/>
<text xml:space="preserve" text-anchor="middle" x="174.96" y="-89.53" font-family="DejaVu Sans" font-size="10.00" fill="#334155"> &#160;Audit&#45;Trail</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Title: d17 Pages: 1 -->
<svg width="654pt" height="232pt"
viewBox="7.00 7.00 646.00 224.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(11.2 220.48)">
<title>d17</title>
<!-- t -->
<g id="node1" class="node">
<title>t</title>
<polygon fill="#1a1a2e" stroke="none" points="12.96,-166.14 12.96,-200.64 618.21,-200.64 618.21,-166.14 12.96,-166.14"/>
<polygon fill="none" stroke="#1a1a2e" points="12.96,-166.14 12.96,-200.64 618.21,-200.64 618.21,-166.14 12.96,-166.14"/>
<text xml:space="preserve" text-anchor="start" x="191.83" y="-180.29" font-family="DejaVu Sans" font-weight="bold" font-size="13.00" fill="#ffffff">DIE 5 FALLSTRICKE IN DER PRAXIS</text>
<polygon fill="#fff1f3" stroke="none" points="12.96,-134.64 12.96,-166.14 230.46,-166.14 230.46,-134.64 12.96,-134.64"/>
<polygon fill="none" stroke="#1a1a2e" points="12.96,-134.64 12.96,-166.14 230.46,-166.14 230.46,-134.64 12.96,-134.64"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-147.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#e11d48">1. &#160;INFEASIBILITY&#45;FALLE</text>
<polygon fill="#ffffff" stroke="none" points="230.46,-134.64 230.46,-166.14 618.21,-166.14 618.21,-134.64 230.46,-134.64"/>
<polygon fill="none" stroke="#1a1a2e" points="230.46,-134.64 230.46,-166.14 618.21,-166.14 618.21,-134.64 230.46,-134.64"/>
<text xml:space="preserve" text-anchor="start" x="239.46" y="-146.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">Unlösbare Modelle durch starre, widersprüchliche Hard Constraints</text>
<polygon fill="#fffbeb" stroke="none" points="12.96,-103.14 12.96,-134.64 230.46,-134.64 230.46,-103.14 12.96,-103.14"/>
<polygon fill="none" stroke="#1a1a2e" points="12.96,-103.14 12.96,-134.64 230.46,-134.64 230.46,-103.14 12.96,-103.14"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-116.19" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#d97706">2. &#160;ERKLÄRBARKEITS&#45;LÜCKE</text>
<polygon fill="#ffffff" stroke="none" points="230.46,-103.14 230.46,-134.64 618.21,-134.64 618.21,-103.14 230.46,-103.14"/>
<polygon fill="none" stroke="#1a1a2e" points="230.46,-103.14 230.46,-134.64 618.21,-134.64 618.21,-103.14 230.46,-103.14"/>
<text xml:space="preserve" text-anchor="start" x="239.46" y="-115.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">Mangelnde Akzeptanz wegen fehlender Begründungen (Black&#45;Box)</text>
<polygon fill="#f5f3ff" stroke="none" points="12.96,-71.64 12.96,-103.14 230.46,-103.14 230.46,-71.64 12.96,-71.64"/>
<polygon fill="none" stroke="#1a1a2e" points="12.96,-71.64 12.96,-103.14 230.46,-103.14 230.46,-71.64 12.96,-71.64"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-84.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#7c3aed">3. &#160;OVERFITTING / REGIME&#45;SHIFT</text>
<polygon fill="#ffffff" stroke="none" points="230.46,-71.64 230.46,-103.14 618.21,-103.14 618.21,-71.64 230.46,-71.64"/>
<polygon fill="none" stroke="#1a1a2e" points="230.46,-71.64 230.46,-103.14 618.21,-103.14 618.21,-71.64 230.46,-71.64"/>
<text xml:space="preserve" text-anchor="start" x="239.46" y="-83.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">Perfekt auf die Vergangenheit optimiert scheitert in Krisen</text>
<polygon fill="#ecfeff" stroke="none" points="12.96,-40.14 12.96,-71.64 230.46,-71.64 230.46,-40.14 12.96,-40.14"/>
<polygon fill="none" stroke="#1a1a2e" points="12.96,-40.14 12.96,-71.64 230.46,-71.64 230.46,-40.14 12.96,-40.14"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-53.19" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#0891b2">4. &#160;DATENVERZUG / LOOKAHEAD</text>
<polygon fill="#ffffff" stroke="none" points="230.46,-40.14 230.46,-71.64 618.21,-71.64 618.21,-40.14 230.46,-40.14"/>
<polygon fill="none" stroke="#1a1a2e" points="230.46,-40.14 230.46,-71.64 618.21,-71.64 618.21,-40.14 230.46,-40.14"/>
<text xml:space="preserve" text-anchor="start" x="239.46" y="-52.19" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">Signale nutzen Daten, die zum Handlungszeitpunkt fehlten</text>
<polygon fill="#eef2ff" stroke="none" points="12.96,-8.64 12.96,-40.14 230.46,-40.14 230.46,-8.64 12.96,-8.64"/>
<polygon fill="none" stroke="#1a1a2e" points="12.96,-8.64 12.96,-40.14 230.46,-40.14 230.46,-8.64 12.96,-8.64"/>
<text xml:space="preserve" text-anchor="start" x="21.96" y="-21.69" font-family="DejaVu Sans" font-weight="bold" font-size="11.00" fill="#4338ca">5. &#160;LAUFZEIT&#45;EXPLOSION</text>
<polygon fill="#ffffff" stroke="none" points="230.46,-8.64 230.46,-40.14 618.21,-40.14 618.21,-8.64 230.46,-8.64"/>
<polygon fill="none" stroke="#1a1a2e" points="230.46,-8.64 230.46,-40.14 618.21,-40.14 618.21,-8.64 230.46,-8.64"/>
<text xml:space="preserve" text-anchor="start" x="239.46" y="-20.69" font-family="DejaVu Sans" font-size="11.00" fill="#1a1a2e">NP&#45;schwere Modelle verfehlen reale Timeouts bei Wachstum</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Pages: 1 -->
<svg width="633pt" height="201pt"
viewBox="0.00 0.00 633.00 201.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 196.84)">
<text xml:space="preserve" text-anchor="middle" x="312.56" y="-177.44" font-family="DejaVu Sans" font-size="12.00">Kostenminimaler Fluss: 140 € für 30 Einheiten</text>
<text xml:space="preserve" text-anchor="middle" x="312.56" y="-163.19" font-family="DejaVu Sans" font-size="12.00">Kantenbeschriftung: Fluss / Kapazität, Kosten je Einheit</text>
<!-- Werk_A -->
<g id="node1" class="node">
<title>Werk_A</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M87.54,-156.34C87.54,-156.34 12,-156.34 12,-156.34 6,-156.34 0,-150.34 0,-144.34 0,-144.34 0,-117.13 0,-117.13 0,-111.13 6,-105.13 12,-105.13 12,-105.13 87.54,-105.13 87.54,-105.13 93.54,-105.13 99.54,-111.13 99.54,-117.13 99.54,-117.13 99.54,-144.34 99.54,-144.34 99.54,-150.34 93.54,-156.34 87.54,-156.34"/>
<text xml:space="preserve" text-anchor="middle" x="49.77" y="-140.36" font-family="DejaVu Sans" font-size="10.00">Werk A</text>
<text xml:space="preserve" text-anchor="middle" x="49.77" y="-127.61" font-family="DejaVu Sans" font-size="10.00">Angebot +20</text>
<text xml:space="preserve" text-anchor="middle" x="49.77" y="-114.86" font-family="DejaVu Sans" font-size="10.00">Knotenpreis 4 €</text>
</g>
<!-- Umschlag -->
<g id="node3" class="node">
<title>Umschlag</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M375.83,-83.34C375.83,-83.34 300.29,-83.34 300.29,-83.34 294.29,-83.34 288.29,-77.34 288.29,-71.34 288.29,-71.34 288.29,-44.13 288.29,-44.13 288.29,-38.13 294.29,-32.13 300.29,-32.13 300.29,-32.13 375.83,-32.13 375.83,-32.13 381.83,-32.13 387.83,-38.13 387.83,-44.13 387.83,-44.13 387.83,-71.34 387.83,-71.34 387.83,-77.34 381.83,-83.34 375.83,-83.34"/>
<text xml:space="preserve" text-anchor="middle" x="338.06" y="-67.36" font-family="DejaVu Sans" font-size="10.00">Umschlag</text>
<text xml:space="preserve" text-anchor="middle" x="338.06" y="-54.61" font-family="DejaVu Sans" font-size="10.00">Durchleitung</text>
<text xml:space="preserve" text-anchor="middle" x="338.06" y="-41.86" font-family="DejaVu Sans" font-size="10.00">Knotenpreis 0 €</text>
</g>
<!-- Werk_A&#45;&gt;Umschlag -->
<g id="edge1" class="edge">
<title>Werk_A&#45;&gt;Umschlag</title>
<path fill="none" stroke="#b45309" stroke-width="4" d="M100.31,-118.1C147.88,-105.97 220.02,-87.58 272.16,-74.29"/>
<polygon fill="#b45309" stroke="#b45309" stroke-width="4" points="272.95,-77.69 281.78,-71.83 271.23,-70.91 272.95,-77.69"/>
<text xml:space="preserve" text-anchor="middle" x="193.91" y="-109.82" font-family="DejaVu Sans" font-size="9.00" fill="#b45309">15 / 15, 2 € &#160;⟵ voll</text>
</g>
<!-- Kunde_1 -->
<g id="node4" class="node">
<title>Kunde_1</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M613.12,-156.34C613.12,-156.34 533.83,-156.34 533.83,-156.34 527.83,-156.34 521.83,-150.34 521.83,-144.34 521.83,-144.34 521.83,-117.13 521.83,-117.13 521.83,-111.13 527.83,-105.13 533.83,-105.13 533.83,-105.13 613.12,-105.13 613.12,-105.13 619.12,-105.13 625.12,-111.13 625.12,-117.13 625.12,-117.13 625.12,-144.34 625.12,-144.34 625.12,-150.34 619.12,-156.34 613.12,-156.34"/>
<text xml:space="preserve" text-anchor="middle" x="573.47" y="-140.36" font-family="DejaVu Sans" font-size="10.00">Kunde 1</text>
<text xml:space="preserve" text-anchor="middle" x="573.47" y="-127.61" font-family="DejaVu Sans" font-size="10.00">Bedarf &#45;15</text>
<text xml:space="preserve" text-anchor="middle" x="573.47" y="-114.86" font-family="DejaVu Sans" font-size="10.00">Knotenpreis &#45;1 €</text>
</g>
<!-- Werk_A&#45;&gt;Kunde_1 -->
<g id="edge2" class="edge">
<title>Werk_A&#45;&gt;Kunde_1</title>
<path fill="none" stroke="#0891b2" stroke-width="2.5" d="M100.32,-130.74C195.01,-130.74 401.63,-130.74 507.34,-130.74"/>
<polygon fill="#0891b2" stroke="#0891b2" stroke-width="2.5" points="507.17,-134.24 517.17,-130.74 507.17,-127.24 507.17,-134.24"/>
<text xml:space="preserve" text-anchor="middle" x="338.06" y="-133.44" font-family="DejaVu Sans" font-size="9.00" fill="#334155">5 / 10, 5 €</text>
</g>
<!-- Werk_B -->
<g id="node2" class="node">
<title>Werk_B</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M87.54,-57.34C87.54,-57.34 12,-57.34 12,-57.34 6,-57.34 0,-51.34 0,-45.34 0,-45.34 0,-18.13 0,-18.13 0,-12.13 6,-6.13 12,-6.13 12,-6.13 87.54,-6.13 87.54,-6.13 93.54,-6.13 99.54,-12.13 99.54,-18.13 99.54,-18.13 99.54,-45.34 99.54,-45.34 99.54,-51.34 93.54,-57.34 87.54,-57.34"/>
<text xml:space="preserve" text-anchor="middle" x="49.77" y="-41.36" font-family="DejaVu Sans" font-size="10.00">Werk B</text>
<text xml:space="preserve" text-anchor="middle" x="49.77" y="-28.61" font-family="DejaVu Sans" font-size="10.00">Angebot +10</text>
<text xml:space="preserve" text-anchor="middle" x="49.77" y="-15.86" font-family="DejaVu Sans" font-size="10.00">Knotenpreis 4 €</text>
</g>
<!-- Werk_B&#45;&gt;Umschlag -->
<g id="edge3" class="edge">
<title>Werk_B&#45;&gt;Umschlag</title>
<path fill="none" stroke="#cbd5e1" stroke-dasharray="5,2" d="M100.31,-36.24C149.22,-40.68 224.08,-47.48 276.49,-52.24"/>
<polygon fill="#cbd5e1" stroke="#cbd5e1" points="275.86,-55.69 286.14,-53.11 276.5,-48.72 275.86,-55.69"/>
<text xml:space="preserve" text-anchor="middle" x="193.91" y="-52.03" font-family="DejaVu Sans" font-size="9.00" fill="#94a3b8">0 / 10, 4 € &#160;(ungenutzt)</text>
</g>
<!-- Kunde_2 -->
<g id="node5" class="node">
<title>Kunde_2</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M613.12,-57.34C613.12,-57.34 533.83,-57.34 533.83,-57.34 527.83,-57.34 521.83,-51.34 521.83,-45.34 521.83,-45.34 521.83,-18.13 521.83,-18.13 521.83,-12.13 527.83,-6.13 533.83,-6.13 533.83,-6.13 613.12,-6.13 613.12,-6.13 619.12,-6.13 625.12,-12.13 625.12,-18.13 625.12,-18.13 625.12,-45.34 625.12,-45.34 625.12,-51.34 619.12,-57.34 613.12,-57.34"/>
<text xml:space="preserve" text-anchor="middle" x="573.47" y="-41.36" font-family="DejaVu Sans" font-size="10.00">Kunde 2</text>
<text xml:space="preserve" text-anchor="middle" x="573.47" y="-28.61" font-family="DejaVu Sans" font-size="10.00">Bedarf &#45;15</text>
<text xml:space="preserve" text-anchor="middle" x="573.47" y="-15.86" font-family="DejaVu Sans" font-size="10.00">Knotenpreis &#45;3 €</text>
</g>
<!-- Werk_B&#45;&gt;Kunde_2 -->
<g id="edge4" class="edge">
<title>Werk_B&#45;&gt;Kunde_2</title>
<path fill="none" stroke="#b45309" stroke-width="4" d="M100.44,-22.29C166.16,-10.96 285.65,5.56 387.83,-2.49 427.11,-5.58 470.66,-12.32 505.6,-18.55"/>
<polygon fill="#b45309" stroke="#b45309" stroke-width="4" points="504.58,-21.92 515.04,-20.27 505.84,-15.03 504.58,-21.92"/>
<text xml:space="preserve" text-anchor="middle" x="338.06" y="-5.19" font-family="DejaVu Sans" font-size="9.00" fill="#b45309">10 / 10, 6 € &#160;⟵ voll</text>
</g>
<!-- Umschlag&#45;&gt;Kunde_1 -->
<g id="edge5" class="edge">
<title>Umschlag&#45;&gt;Kunde_1</title>
<path fill="none" stroke="#0891b2" stroke-width="2.5" d="M388.56,-73.22C423.4,-84.11 470.35,-98.8 508.09,-110.6"/>
<polygon fill="#0891b2" stroke="#0891b2" stroke-width="2.5" points="506.83,-113.87 517.41,-113.52 508.91,-107.19 506.83,-113.87"/>
<text xml:space="preserve" text-anchor="middle" x="454.83" y="-103.48" font-family="DejaVu Sans" font-size="9.00" fill="#334155">10 / 20, 1 €</text>
</g>
<!-- Umschlag&#45;&gt;Kunde_2 -->
<g id="edge6" class="edge">
<title>Umschlag&#45;&gt;Kunde_2</title>
<path fill="none" stroke="#0891b2" stroke-width="2.5" d="M388.56,-52.22C423.24,-48.36 469.94,-43.16 507.6,-38.96"/>
<polygon fill="#0891b2" stroke="#0891b2" stroke-width="2.5" points="507.71,-42.47 517.26,-37.89 506.94,-35.52 507.71,-42.47"/>
<text xml:space="preserve" text-anchor="middle" x="454.83" y="-50.53" font-family="DejaVu Sans" font-size="9.00" fill="#334155">5 / 10, 3 €</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Pages: 1 -->
<svg width="581pt" height="301pt"
viewBox="0.00 0.00 581.00 301.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 296.63)">
<text xml:space="preserve" text-anchor="middle" x="286.68" y="-277.23" font-family="DejaVu Sans" font-size="12.00">Branch&#45;and&#45;Bound: 5 gelöste LPs statt aller ganzzahligen Punkte</text>
<text xml:space="preserve" text-anchor="middle" x="286.68" y="-262.98" font-family="DejaVu Sans" font-size="12.00">max 5x₁ + 4x₂ &#160;u.d.N. &#160;6x₁+4x₂ ≤ 24, &#160;x₁+2x₂ ≤ 6</text>
<!-- P₀ -->
<g id="node1" class="node">
<title>P₀</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M356.91,-256.13C356.91,-256.13 216.88,-256.13 216.88,-256.13 210.88,-256.13 204.88,-250.13 204.88,-244.13 204.88,-244.13 204.88,-216.92 204.88,-216.92 204.88,-210.92 210.88,-204.92 216.88,-204.92 216.88,-204.92 356.91,-204.92 356.91,-204.92 362.91,-204.92 368.91,-210.92 368.91,-216.92 368.91,-216.92 368.91,-244.13 368.91,-244.13 368.91,-250.13 362.91,-256.13 356.91,-256.13"/>
<text xml:space="preserve" text-anchor="middle" x="286.89" y="-240.15" font-family="DejaVu Sans" font-size="10.00">P₀</text>
<text xml:space="preserve" text-anchor="middle" x="286.89" y="-227.4" font-family="DejaVu Sans" font-size="10.00">x = (3; 1,5), Schranke Z ≤ 21</text>
<text xml:space="preserve" text-anchor="middle" x="286.89" y="-214.65" font-family="DejaVu Sans" font-size="10.00">x2 gebrochen — verzweigen</text>
</g>
<!-- A -->
<g id="node2" class="node">
<title>A</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M270.41,-153.67C270.41,-153.67 109.38,-153.67 109.38,-153.67 103.38,-153.67 97.37,-147.67 97.37,-141.67 97.37,-141.67 97.37,-114.46 97.37,-114.46 97.37,-108.46 103.37,-102.46 109.37,-102.46 109.37,-102.46 270.41,-102.46 270.41,-102.46 276.41,-102.46 282.41,-108.46 282.41,-114.46 282.41,-114.46 282.41,-141.67 282.41,-141.67 282.41,-147.67 276.41,-153.67 270.41,-153.67"/>
<text xml:space="preserve" text-anchor="middle" x="189.89" y="-137.69" font-family="DejaVu Sans" font-size="10.00">A</text>
<text xml:space="preserve" text-anchor="middle" x="189.89" y="-124.94" font-family="DejaVu Sans" font-size="10.00">x = (3,33; 1), Schranke Z ≤ 20,67</text>
<text xml:space="preserve" text-anchor="middle" x="189.89" y="-112.19" font-family="DejaVu Sans" font-size="10.00">x1 gebrochen — verzweigen</text>
</g>
<!-- P₀&#45;&gt;A -->
<g id="edge1" class="edge">
<title>P₀&#45;&gt;A</title>
<path fill="none" stroke="#5c6380" d="M262.42,-204.17C250.24,-191.56 235.36,-176.15 222.26,-162.58"/>
<polygon fill="#5c6380" stroke="#5c6380" points="224.88,-160.26 215.42,-155.5 219.84,-165.12 224.88,-160.26"/>
<text xml:space="preserve" text-anchor="middle" x="256.68" y="-176.37" font-family="DejaVu Sans" font-size="9.00">x2 ≤ 1</text>
</g>
<!-- B -->
<g id="node5" class="node">
<title>B</title>
<path fill="#fffbeb" stroke="#b45309" stroke-width="2" d="M439.41,-153.67C439.41,-153.67 326.38,-153.67 326.38,-153.67 320.38,-153.67 314.38,-147.67 314.38,-141.67 314.38,-141.67 314.38,-114.46 314.38,-114.46 314.38,-108.46 320.38,-102.46 326.38,-102.46 326.38,-102.46 439.41,-102.46 439.41,-102.46 445.41,-102.46 451.41,-108.46 451.41,-114.46 451.41,-114.46 451.41,-141.67 451.41,-141.67 451.41,-147.67 445.41,-153.67 439.41,-153.67"/>
<text xml:space="preserve" text-anchor="middle" x="382.89" y="-137.69" font-family="DejaVu Sans" font-size="10.00">B</text>
<text xml:space="preserve" text-anchor="middle" x="382.89" y="-124.94" font-family="DejaVu Sans" font-size="10.00">Z = 18 ≤ Incumbent 20</text>
<text xml:space="preserve" text-anchor="middle" x="382.89" y="-112.19" font-family="DejaVu Sans" font-size="10.00">→ gekappt</text>
</g>
<!-- P₀&#45;&gt;B -->
<g id="edge4" class="edge">
<title>P₀&#45;&gt;B</title>
<path fill="none" stroke="#5c6380" d="M311.12,-204.17C323.17,-191.56 337.9,-176.15 350.87,-162.58"/>
<polygon fill="#5c6380" stroke="#5c6380" points="353.25,-165.15 357.63,-155.5 348.19,-160.31 353.25,-165.15"/>
<text xml:space="preserve" text-anchor="middle" x="353.15" y="-176.37" font-family="DejaVu Sans" font-size="9.00">x2 ≥ 2</text>
</g>
<!-- A1 -->
<g id="node3" class="node">
<title>A1</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M161.79,-51.21C161.79,-51.21 12,-51.21 12,-51.21 6,-51.21 0,-45.21 0,-39.21 0,-39.21 0,-12 0,-12 0,-6 6,0 12,0 12,0 161.79,0 161.79,0 167.79,0 173.79,-6 173.79,-12 173.79,-12 173.79,-39.21 173.79,-39.21 173.79,-45.21 167.79,-51.21 161.79,-51.21"/>
<text xml:space="preserve" text-anchor="middle" x="86.89" y="-35.23" font-family="DejaVu Sans" font-size="10.00">A1</text>
<text xml:space="preserve" text-anchor="middle" x="86.89" y="-22.48" font-family="DejaVu Sans" font-size="10.00">x = (3; 1), Z = 19</text>
<text xml:space="preserve" text-anchor="middle" x="86.89" y="-9.73" font-family="DejaVu Sans" font-size="10.00">ganzzahlig — neuer Incumbent</text>
</g>
<!-- A&#45;&gt;A1 -->
<g id="edge2" class="edge">
<title>A&#45;&gt;A1</title>
<path fill="none" stroke="#5c6380" d="M163.9,-101.71C150.85,-88.98 134.87,-73.4 120.86,-59.74"/>
<polygon fill="#5c6380" stroke="#5c6380" points="123.56,-57.48 113.96,-53 118.68,-62.49 123.56,-57.48"/>
<text xml:space="preserve" text-anchor="middle" x="156.86" y="-73.91" font-family="DejaVu Sans" font-size="9.00">x1 ≤ 3</text>
</g>
<!-- A2 -->
<g id="node4" class="node">
<title>A2</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M367.79,-51.21C367.79,-51.21 218,-51.21 218,-51.21 212,-51.21 206,-45.21 206,-39.21 206,-39.21 206,-12 206,-12 206,-6 212,0 218,0 218,0 367.79,0 367.79,0 373.79,0 379.79,-6 379.79,-12 379.79,-12 379.79,-39.21 379.79,-39.21 379.79,-45.21 373.79,-51.21 367.79,-51.21"/>
<text xml:space="preserve" text-anchor="middle" x="292.89" y="-35.23" font-family="DejaVu Sans" font-size="10.00">A2</text>
<text xml:space="preserve" text-anchor="middle" x="292.89" y="-22.48" font-family="DejaVu Sans" font-size="10.00">x = (4; 0), Z = 20</text>
<text xml:space="preserve" text-anchor="middle" x="292.89" y="-9.73" font-family="DejaVu Sans" font-size="10.00">ganzzahlig — neuer Incumbent</text>
</g>
<!-- A&#45;&gt;A2 -->
<g id="edge3" class="edge">
<title>A&#45;&gt;A2</title>
<path fill="none" stroke="#5c6380" d="M215.89,-101.71C228.94,-88.98 244.92,-73.4 258.93,-59.74"/>
<polygon fill="#5c6380" stroke="#5c6380" points="261.11,-62.49 265.83,-53 256.23,-57.48 261.11,-62.49"/>
<text xml:space="preserve" text-anchor="middle" x="259.86" y="-73.91" font-family="DejaVu Sans" font-size="9.00">x1 ≥ 4</text>
</g>
<!-- ergebnis -->
<g id="node6" class="node">
<title>ergebnis</title>
<polygon fill="#f8fafc" stroke="#94a3b8" points="567.36,-248.53 400.44,-248.53 400.44,-212.53 573.36,-212.53 573.36,-242.53 567.36,-248.53"/>
<polyline fill="none" stroke="#94a3b8" points="567.36,-248.53 567.36,-242.53"/>
<polyline fill="none" stroke="#94a3b8" points="573.36,-242.53 567.36,-242.53"/>
<text xml:space="preserve" text-anchor="middle" x="486.89" y="-227.4" font-family="DejaVu Sans" font-size="10.00">Optimum: x* = (4; 0), Z* = 20</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Pages: 1 -->
<svg width="709pt" height="369pt"
viewBox="0.00 0.00 709.00 369.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 364.66)">
<text xml:space="preserve" text-anchor="middle" x="350.58" y="-344.31" font-family="DejaVu Sans" font-size="13.00">Die Schichten von or_kern.py — alle Pfeile zeigen nach innen</text>
<!-- io -->
<g id="node1" class="node">
<title>io</title>
<path fill="#fffbeb" stroke="#b45309" stroke-width="2" d="M248.04,-237.7C248.04,-237.7 12,-237.7 12,-237.7 6,-237.7 0,-231.7 0,-225.7 0,-225.7 0,-198.49 0,-198.49 0,-192.49 6,-186.49 12,-186.49 12,-186.49 248.04,-186.49 248.04,-186.49 254.04,-186.49 260.04,-192.49 260.04,-198.49 260.04,-198.49 260.04,-225.7 260.04,-225.7 260.04,-231.7 254.04,-237.7 248.04,-237.7"/>
<text xml:space="preserve" text-anchor="middle" x="130.02" y="-221.72" font-family="DejaVu Sans" font-size="10.00">Ein&#45; und Ausgabe</text>
<text xml:space="preserve" text-anchor="middle" x="130.02" y="-208.97" font-family="DejaVu Sans" font-size="10.00">lade_produktionsproblem() · schreibe_ergebnis()</text>
<text xml:space="preserve" text-anchor="middle" x="130.02" y="-196.22" font-family="DejaVu Sans" font-size="10.00">Excel, CSV — die Wirklichkeit des Anwenders</text>
</g>
<!-- dom -->
<g id="node5" class="node">
<title>dom</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M465.04,-139.24C465.04,-139.24 271,-139.24 271,-139.24 265,-139.24 259,-133.24 259,-127.24 259,-127.24 259,-100.03 259,-100.03 259,-94.03 265,-88.03 271,-88.03 271,-88.03 465.04,-88.03 465.04,-88.03 471.04,-88.03 477.04,-94.03 477.04,-100.03 477.04,-100.03 477.04,-127.24 477.04,-127.24 477.04,-133.24 471.04,-139.24 465.04,-139.24"/>
<text xml:space="preserve" text-anchor="middle" x="368.02" y="-123.26" font-family="DejaVu Sans" font-size="10.00">Domäne</text>
<text xml:space="preserve" text-anchor="middle" x="368.02" y="-110.51" font-family="DejaVu Sans" font-size="10.00">Produkt · Produktionsproblem</text>
<text xml:space="preserve" text-anchor="middle" x="368.02" y="-97.76" font-family="DejaVu Sans" font-size="10.00">validiert (Pydantic), kennt keinen Solver</text>
</g>
<!-- io&#45;&gt;dom -->
<g id="edge1" class="edge">
<title>io&#45;&gt;dom</title>
<path fill="none" stroke="#5c6380" d="M153.56,-185.74C166.52,-171.78 180.02,-157.24 180.02,-157.24 180.02,-157.24 210.63,-150.3 247.15,-142.03"/>
<polygon fill="#5c6380" stroke="#5c6380" points="247.66,-145.5 256.64,-139.88 246.11,-138.67 247.66,-145.5"/>
<text xml:space="preserve" text-anchor="middle" x="235.52" y="-159.94" font-family="DejaVu Sans" font-size="9.00">liest ein, schreibt zurück</text>
</g>
<!-- bau -->
<g id="node2" class="node">
<title>bau</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M432.04,-336.16C432.04,-336.16 256,-336.16 256,-336.16 250,-336.16 244,-330.16 244,-324.16 244,-324.16 244,-296.95 244,-296.95 244,-290.95 250,-284.95 256,-284.95 256,-284.95 432.04,-284.95 432.04,-284.95 438.04,-284.95 444.04,-290.95 444.04,-296.95 444.04,-296.95 444.04,-324.16 444.04,-324.16 444.04,-330.16 438.04,-336.16 432.04,-336.16"/>
<text xml:space="preserve" text-anchor="middle" x="344.02" y="-320.18" font-family="DejaVu Sans" font-size="10.00">Modellbauer — austauschbar</text>
<text xml:space="preserve" text-anchor="middle" x="344.02" y="-307.43" font-family="DejaVu Sans" font-size="10.00">loese_mit_glop() · loese_mit_scipy()</text>
<text xml:space="preserve" text-anchor="middle" x="344.02" y="-294.68" font-family="DejaVu Sans" font-size="10.00">je Solver einer, gleiche Signatur</text>
</g>
<!-- erg -->
<g id="node4" class="node">
<title>erg</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M588.66,-237.7C588.66,-237.7 437.38,-237.7 437.38,-237.7 431.38,-237.7 425.38,-231.7 425.38,-225.7 425.38,-225.7 425.38,-198.49 425.38,-198.49 425.38,-192.49 431.38,-186.49 437.38,-186.49 437.38,-186.49 588.66,-186.49 588.66,-186.49 594.66,-186.49 600.66,-192.49 600.66,-198.49 600.66,-198.49 600.66,-225.7 600.66,-225.7 600.66,-231.7 594.66,-237.7 588.66,-237.7"/>
<text xml:space="preserve" text-anchor="middle" x="513.02" y="-221.72" font-family="DejaVu Sans" font-size="10.00">Ergebnis — solverunabhängig</text>
<text xml:space="preserve" text-anchor="middle" x="513.02" y="-208.97" font-family="DejaVu Sans" font-size="10.00">SolverStatus · Loesung</text>
<text xml:space="preserve" text-anchor="middle" x="513.02" y="-196.22" font-family="DejaVu Sans" font-size="10.00">fünf Bibliotheken, eine Sprache</text>
</g>
<!-- bau&#45;&gt;erg -->
<g id="edge3" class="edge">
<title>bau&#45;&gt;erg</title>
<path fill="none" stroke="#5c6380" d="M388.87,-283.96C410.25,-271.76 436.01,-257.05 458.51,-244.21"/>
<polygon fill="#5c6380" stroke="#5c6380" points="459.99,-247.39 466.94,-239.39 456.52,-241.31 459.99,-247.39"/>
<text xml:space="preserve" text-anchor="middle" x="446.34" y="-258.4" font-family="DejaVu Sans" font-size="9.00">liefert</text>
</g>
<!-- bau&#45;&gt;dom -->
<g id="edge2" class="edge">
<title>bau&#45;&gt;dom</title>
<path fill="none" stroke="#5c6380" d="M336.56,-284.02C330.52,-263.37 323.02,-237.7 323.02,-237.7 323.02,-237.7 323.02,-186.49 323.02,-186.49 323.02,-186.49 334.57,-168.05 345.97,-149.85"/>
<polygon fill="#5c6380" stroke="#5c6380" points="348.88,-151.79 351.22,-141.45 342.95,-148.07 348.88,-151.79"/>
<text xml:space="preserve" text-anchor="middle" x="368.02" y="-209.17" font-family="DejaVu Sans" font-size="9.00">baut das Modell aus</text>
</g>
<!-- pruef -->
<g id="node3" class="node">
<title>pruef</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M689.16,-336.16C689.16,-336.16 546.88,-336.16 546.88,-336.16 540.88,-336.16 534.88,-330.16 534.88,-324.16 534.88,-324.16 534.88,-296.95 534.88,-296.95 534.88,-290.95 540.88,-284.95 546.88,-284.95 546.88,-284.95 689.16,-284.95 689.16,-284.95 695.16,-284.95 701.16,-290.95 701.16,-296.95 701.16,-296.95 701.16,-324.16 701.16,-324.16 701.16,-330.16 695.16,-336.16 689.16,-336.16"/>
<text xml:space="preserve" text-anchor="middle" x="618.02" y="-320.18" font-family="DejaVu Sans" font-size="10.00">Abnahmeprüfung</text>
<text xml:space="preserve" text-anchor="middle" x="618.02" y="-307.43" font-family="DejaVu Sans" font-size="10.00">pruefe_loesung()</text>
<text xml:space="preserve" text-anchor="middle" x="618.02" y="-294.68" font-family="DejaVu Sans" font-size="10.00">prüft die Lösung OHNE Solver</text>
</g>
<!-- pruef&#45;&gt;erg -->
<g id="edge5" class="edge">
<title>pruef&#45;&gt;erg</title>
<path fill="none" stroke="#5c6380" d="M590.16,-283.96C577.48,-272.31 562.32,-258.39 548.81,-245.97"/>
<polygon fill="#5c6380" stroke="#5c6380" points="551.49,-243.69 541.76,-239.5 546.76,-248.84 551.49,-243.69"/>
<text xml:space="preserve" text-anchor="middle" x="579.92" y="-258.4" font-family="DejaVu Sans" font-size="9.00">prüft</text>
</g>
<!-- pruef&#45;&gt;dom -->
<g id="edge4" class="edge">
<title>pruef&#45;&gt;dom</title>
<path fill="none" stroke="#5c6380" d="M616.98,-284.06C615.52,-248.01 613.02,-186.49 613.02,-186.49 613.02,-186.49 534.17,-163.36 465.93,-143.35"/>
<polygon fill="#5c6380" stroke="#5c6380" points="467.12,-140.05 456.54,-140.6 465.15,-146.77 467.12,-140.05"/>
<text xml:space="preserve" text-anchor="middle" x="642.02" y="-209.17" font-family="DejaVu Sans" font-size="9.00">prüft gegen</text>
</g>
<!-- erg&#45;&gt;dom -->
<!-- regel -->
<g id="node6" class="node">
<title>regel</title>
<polygon fill="#f8fafc" stroke="#94a3b8" points="487.85,-51.03 242.19,-51.03 242.19,0 493.85,0 493.85,-45.03 487.85,-51.03"/>
<polyline fill="none" stroke="#94a3b8" points="487.85,-51.03 487.85,-45.03"/>
<polyline fill="none" stroke="#94a3b8" points="493.85,-45.03 487.85,-45.03"/>
<text xml:space="preserve" text-anchor="start" x="255.14" y="-33.84" font-family="DejaVu Sans" font-size="9.00">Die Domäne importiert nichts von außen.</text>
<text xml:space="preserve" text-anchor="start" x="255.14" y="-22.59" font-family="DejaVu Sans" font-size="9.00">Deshalb kostet ein Solverwechsel eine Funktion —</text>
<text xml:space="preserve" text-anchor="start" x="255.14" y="-11.34" font-family="DejaVu Sans" font-size="9.00">und kein einziges Fachdetail.</text>
</g>
<!-- dom&#45;&gt;regel -->
<g id="edge7" class="edge">
<title>dom&#45;&gt;regel</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M368.02,-87.26C368.02,-75.95 368.02,-62.67 368.02,-51.41"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
bilder_04/kap_qp_kkt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

3473
bilder_04/kap_qp_kkt.svg Normal file

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Pages: 1 -->
<svg width="496pt" height="553pt"
viewBox="0.00 0.00 496.00 553.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 549.04)">
<text xml:space="preserve" text-anchor="middle" x="244.04" y="-528.69" font-family="DejaVu Sans" font-size="13.00">Optimierungsdienst: warum die Antwort 202 lautet, nicht 200</text>
<!-- klient -->
<g id="node1" class="node">
<title>klient</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M237.91,-520.54C237.91,-520.54 129.38,-520.54 129.38,-520.54 123.38,-520.54 117.37,-514.54 117.37,-508.54 117.37,-508.54 117.37,-494.08 117.37,-494.08 117.37,-488.08 123.37,-482.08 129.38,-482.08 129.38,-482.08 237.91,-482.08 237.91,-482.08 243.91,-482.08 249.91,-488.08 249.91,-494.08 249.91,-494.08 249.91,-508.54 249.91,-508.54 249.91,-514.54 243.91,-520.54 237.91,-520.54"/>
<text xml:space="preserve" text-anchor="middle" x="183.64" y="-504.56" font-family="DejaVu Sans" font-size="10.00">Aufrufer</text>
<text xml:space="preserve" text-anchor="middle" x="183.64" y="-491.81" font-family="DejaVu Sans" font-size="10.00">(curl, Fachanwendung)</text>
</g>
<!-- post -->
<g id="node2" class="node">
<title>post</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M394.04,-445.08C394.04,-445.08 283.25,-445.08 283.25,-445.08 277.25,-445.08 271.25,-439.08 271.25,-433.08 271.25,-433.08 271.25,-418.62 271.25,-418.62 271.25,-412.62 277.25,-406.62 283.25,-406.62 283.25,-406.62 394.04,-406.62 394.04,-406.62 400.04,-406.62 406.04,-412.62 406.04,-418.62 406.04,-418.62 406.04,-433.08 406.04,-433.08 406.04,-439.08 400.04,-445.08 394.04,-445.08"/>
<text xml:space="preserve" text-anchor="middle" x="338.64" y="-429.1" font-family="DejaVu Sans" font-size="10.00">POST /plaene</text>
<text xml:space="preserve" text-anchor="middle" x="338.64" y="-416.35" font-family="DejaVu Sans" font-size="10.00">JSON mit dem Problem</text>
</g>
<!-- klient&#45;&gt;post -->
<g id="edge1" class="edge">
<title>klient&#45;&gt;post</title>
<path fill="none" stroke="#5c6380" d="M223.96,-481.2C243.53,-471.93 267.24,-460.69 288.02,-450.85"/>
<polygon fill="#5c6380" stroke="#5c6380" points="289.35,-454.08 296.89,-446.64 286.35,-447.76 289.35,-454.08"/>
</g>
<!-- get -->
<g id="node7" class="node">
<title>get</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M157.29,-44.75C157.29,-44.75 12,-44.75 12,-44.75 6,-44.75 0,-38.75 0,-32.75 0,-32.75 0,-18.28 0,-18.28 0,-12.28 6,-6.29 12,-6.29 12,-6.29 157.29,-6.29 157.29,-6.29 163.29,-6.29 169.29,-12.29 169.29,-18.29 169.29,-18.29 169.29,-32.75 169.29,-32.75 169.29,-38.75 163.29,-44.75 157.29,-44.75"/>
<text xml:space="preserve" text-anchor="middle" x="84.64" y="-28.77" font-family="DejaVu Sans" font-size="10.00">GET /plaene/{id}</text>
<text xml:space="preserve" text-anchor="middle" x="84.64" y="-16.02" font-family="DejaVu Sans" font-size="10.00">laeuft · fertig · fehlgeschlagen</text>
</g>
<!-- klient&#45;&gt;get -->
<g id="edge7" class="edge">
<title>klient&#45;&gt;get</title>
<path fill="none" stroke="#5c6380" d="M157.63,-481.2C128,-459.44 83.64,-426.85 83.64,-426.85 83.64,-426.85 83.64,-426.85 83.64,-116.51 83.64,-116.51 84.02,-83.05 84.31,-56.94"/>
<polygon fill="#5c6380" stroke="#5c6380" points="87.8,-57.15 84.42,-47.11 80.81,-57.07 87.8,-57.15"/>
<text xml:space="preserve" text-anchor="middle" x="107.27" y="-255.9" font-family="DejaVu Sans" font-size="9.00">fragt nach</text>
</g>
<!-- gesund -->
<g id="node8" class="node">
<title>gesund</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M233.79,-445.08C233.79,-445.08 133.5,-445.08 133.5,-445.08 127.5,-445.08 121.5,-439.08 121.5,-433.08 121.5,-433.08 121.5,-418.62 121.5,-418.62 121.5,-412.62 127.5,-406.62 133.5,-406.62 133.5,-406.62 233.79,-406.62 233.79,-406.62 239.79,-406.62 245.79,-412.62 245.79,-418.62 245.79,-418.62 245.79,-433.08 245.79,-433.08 245.79,-439.08 239.79,-445.08 233.79,-445.08"/>
<text xml:space="preserve" text-anchor="middle" x="183.64" y="-429.1" font-family="DejaVu Sans" font-size="10.00">GET /gesundheit</text>
<text xml:space="preserve" text-anchor="middle" x="183.64" y="-416.35" font-family="DejaVu Sans" font-size="10.00">für die Überwachung</text>
</g>
<!-- klient&#45;&gt;gesund -->
<g id="edge9" class="edge">
<title>klient&#45;&gt;gesund</title>
<path fill="none" stroke="#5c6380" stroke-dasharray="1,5" d="M183.64,-481.39C183.64,-474.08 183.64,-465.53 183.64,-457.39"/>
<polygon fill="#5c6380" stroke="#5c6380" points="187.15,-457.47 183.65,-447.47 180.15,-457.47 187.15,-457.47"/>
</g>
<!-- valid -->
<g id="node3" class="node">
<title>valid</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M317.6,-354.68C317.6,-354.68 249.15,-325.6 249.15,-325.6 243.63,-323.26 243.63,-318.56 249.15,-316.22 249.15,-316.22 317.6,-287.14 317.6,-287.14 323.12,-284.8 334.17,-284.8 339.69,-287.14 339.69,-287.14 408.14,-316.22 408.14,-316.22 413.66,-318.56 413.66,-323.26 408.14,-325.6 408.14,-325.6 339.69,-354.68 339.69,-354.68 334.17,-357.02 323.12,-357.02 317.6,-354.68"/>
<text xml:space="preserve" text-anchor="middle" x="328.64" y="-324.16" font-family="DejaVu Sans" font-size="10.00">Pydantic prüft</text>
<text xml:space="preserve" text-anchor="middle" x="328.64" y="-311.41" font-family="DejaVu Sans" font-size="10.00">die Eingabe</text>
</g>
<!-- post&#45;&gt;valid -->
<g id="edge2" class="edge">
<title>post&#45;&gt;valid</title>
<path fill="none" stroke="#5c6380" d="M336.81,-405.97C335.82,-395.73 334.54,-382.58 333.29,-369.73"/>
<polygon fill="#5c6380" stroke="#5c6380" points="336.81,-369.71 332.35,-360.1 329.84,-370.39 336.81,-369.71"/>
</g>
<!-- fehler -->
<g id="node4" class="node">
<title>fehler</title>
<path fill="#fffbeb" stroke="#b45309" stroke-width="2" d="M247.29,-235.2C247.29,-235.2 144,-235.2 144,-235.2 138,-235.2 132,-229.2 132,-223.2 132,-223.2 132,-195.99 132,-195.99 132,-189.99 138,-183.99 144,-183.99 144,-183.99 247.29,-183.99 247.29,-183.99 253.29,-183.99 259.29,-189.99 259.29,-195.99 259.29,-195.99 259.29,-223.2 259.29,-223.2 259.29,-229.2 253.29,-235.2 247.29,-235.2"/>
<text xml:space="preserve" text-anchor="middle" x="195.64" y="-219.22" font-family="DejaVu Sans" font-size="10.00">422</text>
<text xml:space="preserve" text-anchor="middle" x="195.64" y="-206.47" font-family="DejaVu Sans" font-size="10.00">Fehler benannt,</text>
<text xml:space="preserve" text-anchor="middle" x="195.64" y="-193.72" font-family="DejaVu Sans" font-size="10.00">bevor ein Solver läuft</text>
</g>
<!-- valid&#45;&gt;fehler -->
<g id="edge3" class="edge">
<title>valid&#45;&gt;fehler</title>
<path fill="none" stroke="#5c6380" d="M297.8,-294.56C279.3,-279.35 255.57,-259.85 235.64,-243.47"/>
<polygon fill="#5c6380" stroke="#5c6380" points="237.9,-240.8 227.95,-237.15 233.46,-246.2 237.9,-240.8"/>
<text xml:space="preserve" text-anchor="middle" x="277.28" y="-255.9" font-family="DejaVu Sans" font-size="9.00">ungültig</text>
</g>
<!-- an -->
<g id="node5" class="node">
<title>an</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M361.16,-228.83C361.16,-228.83 296.12,-228.83 296.12,-228.83 290.12,-228.83 284.12,-222.83 284.12,-216.83 284.12,-216.83 284.12,-202.37 284.12,-202.37 284.12,-196.37 290.12,-190.37 296.12,-190.37 296.12,-190.37 361.16,-190.37 361.16,-190.37 367.16,-190.37 373.16,-196.37 373.16,-202.37 373.16,-202.37 373.16,-216.83 373.16,-216.83 373.16,-222.83 367.16,-228.83 361.16,-228.83"/>
<text xml:space="preserve" text-anchor="middle" x="328.64" y="-212.85" font-family="DejaVu Sans" font-size="10.00">202 Accepted</text>
<text xml:space="preserve" text-anchor="middle" x="328.64" y="-200.1" font-family="DejaVu Sans" font-size="10.00">+ Auftrags&#45;ID</text>
</g>
<!-- valid&#45;&gt;an -->
<g id="edge4" class="edge">
<title>valid&#45;&gt;an</title>
<path fill="none" stroke="#5c6380" d="M328.64,-281.37C328.64,-268.14 328.64,-253.53 328.64,-241.03"/>
<polygon fill="#5c6380" stroke="#5c6380" points="332.15,-241.33 328.65,-231.33 325.15,-241.33 332.15,-241.33"/>
<text xml:space="preserve" text-anchor="middle" x="341.77" y="-255.9" font-family="DejaVu Sans" font-size="9.00">gültig</text>
</g>
<!-- an&#45;&gt;klient -->
<g id="edge6" class="edge">
<title>an&#45;&gt;klient</title>
<path fill="none" stroke="#5c6380" stroke-dasharray="5,2" d="M355.98,-229.66C386.49,-250.95 431.64,-282.45 431.64,-282.45 431.64,-282.45 431.64,-359.37 431.64,-359.37 431.64,-359.37 418.64,-445.08 418.64,-445.08 418.64,-445.08 329.88,-465.94 261.46,-482.02"/>
<polygon fill="#5c6380" stroke="#5c6380" points="261.08,-478.52 252.15,-484.21 262.68,-485.33 261.08,-478.52"/>
<text xml:space="preserve" text-anchor="middle" x="458.46" y="-380.07" font-family="DejaVu Sans" font-size="9.00">sofort zurück</text>
</g>
<!-- rechne -->
<g id="node6" class="node">
<title>rechne</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M368.54,-136.74C368.54,-136.74 266.75,-136.74 266.75,-136.74 260.75,-136.74 254.75,-130.74 254.75,-124.74 254.75,-124.74 254.75,-110.28 254.75,-110.28 254.75,-104.28 260.75,-98.28 266.75,-98.28 266.75,-98.28 368.54,-98.28 368.54,-98.28 374.54,-98.28 380.54,-104.28 380.54,-110.28 380.54,-110.28 380.54,-124.74 380.54,-124.74 380.54,-130.74 374.54,-136.74 368.54,-136.74"/>
<text xml:space="preserve" text-anchor="middle" x="317.64" y="-120.76" font-family="DejaVu Sans" font-size="10.00">Hintergrundrechnung</text>
<text xml:space="preserve" text-anchor="middle" x="317.64" y="-108.01" font-family="DejaVu Sans" font-size="10.00">rechne() → or_kern</text>
</g>
<!-- an&#45;&gt;rechne -->
<g id="edge5" class="edge">
<title>an&#45;&gt;rechne</title>
<path fill="none" stroke="#5c6380" d="M326.31,-189.49C324.87,-177.67 322.98,-162.22 321.34,-148.76"/>
<polygon fill="#5c6380" stroke="#5c6380" points="324.84,-148.59 320.16,-139.09 317.9,-149.44 324.84,-148.59"/>
<text xml:space="preserve" text-anchor="middle" x="338.14" y="-157.44" font-family="DejaVu Sans" font-size="9.00">startet</text>
</g>
<!-- rechne&#45;&gt;get -->
<g id="edge8" class="edge">
<title>rechne&#45;&gt;get</title>
<path fill="none" stroke="#5c6380" stroke-dasharray="5,2" d="M258.5,-97.33C231.69,-88.64 205.89,-80.28 205.89,-80.28 205.89,-80.28 170.63,-64.64 138.14,-50.24"/>
<polygon fill="#5c6380" stroke="#5c6380" points="139.68,-47.09 129.12,-46.24 136.84,-53.49 139.68,-47.09"/>
<text xml:space="preserve" text-anchor="middle" x="258.77" y="-71.73" font-family="DejaVu Sans" font-size="9.00">hinterlegt das Ergebnis</text>
</g>
<!-- regel -->
<g id="node9" class="node">
<title>regel</title>
<polygon fill="#f8fafc" stroke="#94a3b8" points="434.48,-51.03 194.81,-51.03 194.81,0 440.48,0 440.48,-45.03 434.48,-51.03"/>
<polyline fill="none" stroke="#94a3b8" points="434.48,-51.03 434.48,-45.03"/>
<polyline fill="none" stroke="#94a3b8" points="440.48,-45.03 434.48,-45.03"/>
<text xml:space="preserve" text-anchor="start" x="207.77" y="-33.84" font-family="DejaVu Sans" font-size="9.00">Eine Optimierung dauert Sekunden bis Minuten.</text>
<text xml:space="preserve" text-anchor="start" x="207.77" y="-22.59" font-family="DejaVu Sans" font-size="9.00">Synchron beantwortet, blockiert sie den Dienst</text>
<text xml:space="preserve" text-anchor="start" x="207.77" y="-11.34" font-family="DejaVu Sans" font-size="9.00">und läuft in jedes Gateway&#45;Timeout.</text>
</g>
<!-- rechne&#45;&gt;regel -->
<g id="edge10" class="edge">
<title>rechne&#45;&gt;regel</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M317.64,-97.42C317.64,-84.01 317.64,-65.93 317.64,-51.39"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,123 @@
<style>
:root{
--ink:#1A1A2E; --indigo:#4338CA; --teal:#0891B2; --violet:#7C3AED;
--emerald:#059669; --amber:#D97706; --rose:#E11D48; --slate:#334155;
--tint-indigo:#EEF2FF; --tint-violet:#F5F3FF; --line:#E2E8F0;
--code-bg:#1e293b;
}
*{box-sizing:border-box;}
html{scroll-behavior:smooth;}
body{
font-family:"Segoe UI",-apple-system,BlinkMacSystemFont,"DejaVu Sans",Arial,sans-serif;
color:var(--ink); background:#ffffff; line-height:1.7;
max-width:900px; margin:0 auto; padding:1.2rem 1.4rem 6rem;
font-size:17px; -webkit-text-size-adjust:100%;
}
h1,h2,h3,h4,h5{line-height:1.25; font-weight:700; margin-top:2.2em; margin-bottom:.6em;
scroll-margin-top:1rem;}
h1{color:var(--indigo); font-size:2.15rem; border-bottom:4px solid var(--indigo); padding-bottom:.25em;}
h2{color:var(--indigo); font-size:1.6rem; border-bottom:2px solid var(--line); padding-bottom:.2em;}
h3{color:var(--teal); font-size:1.28rem;}
h4{color:var(--violet); font-size:1.1rem;}
h5{color:var(--slate); font-size:1rem; text-transform:uppercase; letter-spacing:.03em;}
a{color:var(--indigo); text-decoration:none;}
a:hover{text-decoration:underline;}
/* Externe Links: Öffnen im neuen Tab + Markierung */
a[target="_blank"]::after{content:" \2197"; font-size:.82em; color:var(--teal); text-decoration:none;}
p{margin:.75em 0;}
hr{border:0; border-top:1px solid var(--line); margin:2.2em 0;}
/* Inhaltsverzeichnis */
nav#TOC{background:var(--tint-indigo); border:1px solid #C7D2FE; border-radius:12px;
padding:1.1em 1.6em 1.3em; margin:2em 0;}
nav#TOC::before{content:"Inhalt"; display:block; font-weight:700; color:var(--indigo);
font-size:1.35rem; margin-bottom:.4em;}
nav#TOC ul{list-style:none; padding-left:1.1em; margin:.15em 0;}
nav#TOC > ul{padding-left:0;}
nav#TOC li{margin:.22em 0;}
nav#TOC a{color:var(--slate); text-decoration:none;}
nav#TOC a:hover{color:var(--indigo); text-decoration:underline;}
nav#TOC > ul > li > a{font-weight:700; color:var(--indigo);}
nav#TOC a[target="_blank"]::after{content:"";} /* keine Pfeile im TOC */
/* Bilder / Figuren */
figure{margin:2em 0; text-align:center;}
img{max-width:100%; height:auto; display:block; margin:0 auto; border-radius:10px;}
figure img{box-shadow:0 6px 24px rgba(26,26,46,.10);}
figcaption{color:var(--slate); font-size:.92rem; font-style:italic; margin-top:.7em;}
body > figure:first-of-type img{box-shadow:0 10px 40px rgba(67,56,202,.15);}
body > figure:first-of-type figcaption{display:none;}
/* Tabellen */
table{border-collapse:collapse; width:100%; margin:1.4em 0; font-size:.96rem;
box-shadow:0 2px 10px rgba(26,26,46,.06); border-radius:8px; overflow:hidden;}
th{background:var(--indigo); color:#fff; font-weight:700; text-align:left;}
th,td{padding:.6em .8em; border:1px solid var(--line); vertical-align:top;}
tbody tr:nth-child(even){background:var(--tint-indigo);}
/* Code dunkler Kasten, heller Text (breezedark) */
code{font-family:"DejaVu Sans Mono",Consolas,Menlo,monospace; font-size:.9em;
background:var(--tint-violet); color:#5b21b6; padding:.12em .4em; border-radius:5px;}
pre{background:var(--code-bg); color:#e6edf3; padding:1.1em 1.2em; border-radius:10px;
overflow-x:auto; line-height:1.5; font-size:.86rem;
box-shadow:0 4px 18px rgba(15,23,42,.25);}
pre code{background:none; color:inherit; padding:0; font-size:inherit;}
pre .co{color:#8b9bb4 !important; font-style:italic;} /* Kommentare gut lesbar */
/* Copy-Button für Codeblöcke */
div.sourceCode{position:relative;}
div.sourceCode pre.sourceCode{padding-top:2.6em;}
.copy-btn{
position:absolute; top:.6em; right:.6em; z-index:2;
display:flex; align-items:center; gap:.35em;
background:rgba(255,255,255,.08); color:#cbd5e1;
border:1px solid rgba(255,255,255,.18); border-radius:6px;
padding:.3em .65em; font:inherit; font-size:.78rem;
cursor:pointer; transition:background .15s,color .15s,border-color .15s;
}
.copy-btn:hover{background:rgba(255,255,255,.18); color:#fff;}
.copy-btn svg{width:1em; height:1em; flex:none;}
.copy-btn.copied{background:var(--emerald); color:#fff; border-color:var(--emerald);}
/* Blockquote */
blockquote{border-left:4px solid var(--violet); background:var(--tint-violet);
margin:1.4em 0; padding:.6em 1.2em; border-radius:0 8px 8px 0; color:#3b2e63;}
/* Mathe */
.katex-display{overflow-x:auto; overflow-y:hidden; padding:.4em 0;}
ul,ol{padding-left:1.5em;} li{margin:.3em 0;}
@media (max-width:640px){ body{font-size:16px; padding:1rem;} h1{font-size:1.8rem;} }
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("div.sourceCode").forEach(function (box) {
var code = box.querySelector("code");
if (!code) return;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "copy-btn";
btn.setAttribute("aria-label", "Code kopieren");
btn.innerHTML =
'<svg 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"/></svg>' +
'<span>Kopieren</span>';
btn.addEventListener("click", function () {
navigator.clipboard.writeText(code.innerText).then(function () {
var label = btn.querySelector("span");
var original = label.textContent;
btn.classList.add("copied");
label.textContent = "Kopiert!";
setTimeout(function () {
btn.classList.remove("copied");
label.textContent = original;
}, 1500);
});
});
box.appendChild(btn);
});
});
</script>

176
bilder_04/or_pdf_header.tex Normal file
View file

@ -0,0 +1,176 @@
% or_pdf_header.tex
% Gemeinsame LaTeX-Zusatzpraeambel fuer den PDF-Bau der Arbeitskopie UND
% der Version 02. Einbindung per pandoc --include-in-header <diese Datei>.
% --- SVG-Grafiken auf die Textbreite begrenzen -----------------------------
% Pandoc setzt \setkeys{Gin}{width=\maxwidth,...}, damit zu breite Bilder
% automatisch verkleinert werden. Das wirkt aber nur auf \includegraphics.
% SVG-Bilder bindet pandoc als \includesvg{...} OHNE Breitenangabe ein; das
% Paket svg rendert sie dann in ihrer natuerlichen Groesse - und alles, was
% breiter als der Textblock ist, laeuft ueber den rechten Rand hinaus.
%
% Der folgende Wrapper stellt das pandoc-Verhalten fuer \includesvg her:
% Bilder werden auf \linewidth verkleinert, wenn sie breiter sind, und
% ansonsten unveraendert gesetzt (also nie kuenstlich vergroessert).
%
% \LetLtxMacro statt \let ist noetig, weil \includesvg ein optionales
% Argument hat; ein einfaches \let wuerde zu einer Endlosrekursion fuehren.
%
% Die Abfrage auf \includesvg ist wichtig: Pandoc laedt das Paket svg nur
% dann, wenn das Dokument ueberhaupt SVG-Bilder enthaelt. Ohne die Abfrage
% scheitert der Lauf bei Dokumenten ohne SVG mit "Undefined control sequence".
\usepackage{letltxmacro}
\makeatletter
\@ifundefined{includesvg}{}{%
\LetLtxMacro\origincludesvg\includesvg
\renewcommand*{\includesvg}[2][]{%
\begingroup
\sbox0{\origincludesvg[#1]{#2}}%
\ifdim\wd0>\linewidth
\resizebox{\linewidth}{!}{\usebox0}%
\else
\usebox0
\fi
\endgroup}%
}
\makeatother
% --- Lange Codezeilen umbrechen statt ueber den Rand laufen lassen ----------
% fvextra erweitert die von pandoc verwendete fancyvrb-Umgebung um
% Zeilenumbruch. Ohne das werden Zeilen jenseits der Textbreite abgeschnitten.
\usepackage{fvextra}
\fvset{
breaklines=true, % lange Zeilen umbrechen
breakanywhere=true, % notfalls auch mitten im Wort (lange Strings)
breaksymbolleft={\tiny\ensuremath{\hookrightarrow}}, % Fortsetzungszeichen
fontsize=\small,
}
% Auch fuer die von pandoc erzeugte Highlighting-Umgebung wirksam machen
% (das sind die Bloecke mit Sprachangabe, z. B. ```python).
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{
commandchars=\\\{\},
breaklines=true,
breakanywhere=true,
breaksymbolleft={\tiny\ensuremath{\hookrightarrow}},
fontsize=\small
}
% Bloecke OHNE Sprachangabe (```) erzeugt pandoc als klassisches verbatim.
% Das kennt keinen Zeilenumbruch und laeuft daher ebenfalls ueber den Rand -
% betroffen sind hier vor allem die abgedruckten Programmausgaben.
% \RecustomVerbatimEnvironment baut verbatim auf fancyvrbs Verbatim um.
\RecustomVerbatimEnvironment{verbatim}{Verbatim}{
breaklines=true,
breakanywhere=true,
breaksymbolleft={\tiny\ensuremath{\hookrightarrow}},
fontsize=\small
}
% --- Tabellen etwas kompakter, damit breite Tabellen besser passen ---------
\usepackage{array}
\renewcommand{\arraystretch}{1.08}
% --- Umbruchverhalten fuer lange URLs und Bezeichner -----------------------
% Lange Bezeichner wie Kap_05_MILP_Portfolio_Fixgebuehren.py setzt pandoc als
% \texttt{...}. LaTeX trennt darin nicht und laesst solche Namen ueber den
% rechten Rand laufen, wenn sie ans Zeilenende geraten. Ein \allowbreak nach
% jedem Unterstrich erlaubt einen Umbruch an genau diesen Stellen.
% Bewusst OHNE Trennstrich: Ein Bindestrich waere hier irrefuehrend, weil er
% wie ein Bestandteil des Dateinamens aussaehe.
\let\origtextunderscore\_
\renewcommand{\_}{\origtextunderscore\allowbreak}
\sloppy % lieber grosszuegigere Wortabstaende als Ueberhang
\emergencystretch=3em
% --- Stichwortregister (Phase 4) --------------------------------------------
% Begriffe werden im Markdown mit {idx:Begriff} bzw. fuer Unterbegriffe mit
% {idx:Oberbegriff!Unterbegriff} markiert (siehe resolve_index() in
% build_version_03.py) und dort zu einem rohen `\index{...}`{=latex}-Block
% aufgeloest. Fuer HTML wird dieser Rohblock von Pandoc automatisch verworfen
% (kein Seitenzahlen-Konzept dort) - kein Sonderfall im Buildskript notwendig.
% Sortierung uebernimmt texindy (xindy mit German-Modul) statt makeindex,
% damit Umlaute korrekt einsortiert werden (ä nach a, ö nach o, ü nach u),
% ohne dass Autor:innen von Hand ASCII-Sortierschluessel angeben muessen.
\usepackage{makeidx}
\makeindex
% --- Modernes Layout: Ueberschriften, Kopfzeile, Umbruch (Phase 5) ----------
% Farben aus der Titelseiten-Palette (bilder/titelseite.svg), damit das PDF
% und das Deckblatt als ein zusammenhaengendes System wirken.
\usepackage{xcolor}
\definecolor{ORindigo}{HTML}{4338CA}
\definecolor{ORcyan}{HTML}{0891B2}
\definecolor{ORviolett}{HTML}{7C3AED}
\definecolor{ORslate}{HTML}{1A1A2E}
% --- Witwen-/Waisenstrafen (e-TeX) ---
% Schusterjungen (erste Zeile eines Absatzes allein am Seitenende) und
% Hurenkinder (letzte Zeile allein am Seitenanfang) weitgehend unterdruecken.
% \widows=1/\orphans=1 waeren zusaetzlich, sind aber in der article-Klasse
% nicht definiert (KOMA/widows-Paket-Syntax) - \widowpenalty/\clubpenalty
% reichen als Strafen.
\widowpenalty=10000
\clubpenalty=10000
% --- titlesec: modernere Ueberschriften + keine Waisenueberschriften ---
% Option nobottomtitles: rutscht eine Ueberschrift ans Seitenende, ohne dass
% noch ausreichend Folgeplatz (Default: ~2\baselineskip) bleibt, wird sie auf
% die naechste Seite geschoben. explicit: ermoeglicht #1 (Titeltext) im after-
% code, um \sectionmark fuer die Kopfzeile manuell zu setzen (noetig, weil
% Pandoc \section* verwendet, das \rightmark nicht automatisch befuellt).
\usepackage[explicit,nobottomtitles]{titlesec}
\titleformat{\section}[block]
{\Large\bfseries\color{ORindigo}}
{}{0pt}
{#1}
[\vspace{2pt}{\color{ORviolett}\titlerule[1.2pt]}\vspace{2pt}\sectionmark{#1}]
\titleformat{\subsection}[block]
{\large\bfseries\color{ORcyan}}
{}{0pt}
{#1}[\subsectionmark{#1}]
\titleformat{\subsubsection}[block]
{\normalsize\bfseries\color{ORslate}}
{}{0pt}
{#1}
\titlespacing*{\section}{0pt}{2.5ex plus 1ex minus .2ex}{1.2ex plus .2ex}
\titlespacing*{\subsection}{0pt}{2ex plus .8ex}{1ex plus .2ex}
\titlespacing*{\subsubsection}{0pt}{1.5ex plus .6ex}{0.8ex plus .1ex}
% --- Sicherungsnetz: needspace fuer den Fall, dass nobottomtitles greift ---
% titlesec hat \section bereits umdefiniert; \preto haengt \needspace vorne an,
% wirkt also auf \section und \section* gleichermaessen. 3\baselineskip
% garantiert mindestens 3 Folgezeilen unter der Ueberschrift.
\usepackage{needspace}
\usepackage{etoolbox}
\preto\section{\needspace{3\baselineskip}}
\preto\subsection{\needspace{2.5\baselineskip}}
\preto\subsubsection{\needspace{2\baselineskip}}
% --- Kopf-/Fusszeile (fancyhdr) ---
% article ist einseitig (oneside) -> es gibt nur L/R, kein Innen/Aussen.
% \rightmark = aktueller Abschnittstitel (article: \sectionmark -> \markright).
% Wir setzen \sectionmark explizit via titlesec-after-code, weil \section* den
% Mark nicht selbst befuellt. \nouppercase laesst Gross-/Kleinschreibung intakt.
\usepackage{fancyhdr}
% Kopfzeile braucht ~22pt Hoehe (fancyhdr warnt sonst); topmargin anpassen,
% damit der Textkörper nicht nach unten wandert.
\setlength{\headheight}{22pt}
\addtolength{\topmargin}{-10pt}
\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{\small\nouppercase{\rightmark}}
\fancyhead[R]{\small\thepage}
\fancyfoot{}
\renewcommand{\headrulewidth}{0.4pt}
\renewcommand{\headrule}{%
\hbox to\headwidth{\color{ORviolett}\leaders\hrule height\headrulewidth\hfill}}
% plain = erste Seite eines (mit \clearpage begonnenen) Kapitels: nur Seitenzahl.
\fancypagestyle{plain}{%
\fancyhf{}\fancyfoot[C]{\small\thepage}\renewcommand{\headrulewidth}{0pt}}
% --- Titelseite (Seite 1, reines Bild) bekommt keine Kopfzeile ---
% Ohne diese Zeile erbt Seite 1 den globalen fancy-Stil (Trennlinie +
% Seitenzahl oben rechts), was auf dem Deckblatt fehl am Platz ist.
\AtBeginDocument{\thispagestyle{plain}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View file

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Pages: 1 -->
<svg width="740pt" height="532pt"
viewBox="0.00 0.00 740.00 532.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 527.75)">
<text xml:space="preserve" text-anchor="middle" x="365.89" y="-507.4" font-family="DejaVu Sans" font-size="13.00">Welches Werkzeug passt zu meinem Problem? (Teil II)</text>
<!-- f1 -->
<g id="node1" class="node">
<title>f1</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="538.52,-499.25 434,-470.87 538.52,-442.49 643.04,-470.87 538.52,-499.25"/>
<text xml:space="preserve" text-anchor="middle" x="538.52" y="-474.12" font-family="DejaVu Sans" font-size="10.00">Fahrzeuge, Depots,</text>
<text xml:space="preserve" text-anchor="middle" x="538.52" y="-461.37" font-family="DejaVu Sans" font-size="10.00">Zeitfenster?</text>
</g>
<!-- f2 -->
<g id="node2" class="node">
<title>f2</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="422.52,-399.24 270.75,-370.86 422.52,-342.48 574.29,-370.86 422.52,-399.24"/>
<text xml:space="preserve" text-anchor="middle" x="422.52" y="-374.11" font-family="DejaVu Sans" font-size="10.00">Ja/Nein&#45;Entscheidungen,</text>
<text xml:space="preserve" text-anchor="middle" x="422.52" y="-361.36" font-family="DejaVu Sans" font-size="10.00">Zuordnungen, Reihenfolgen?</text>
</g>
<!-- f1&#45;&gt;f2 -->
<g id="edge2" class="edge">
<title>f1&#45;&gt;f2</title>
<path fill="none" stroke="#5c6380" d="M513.35,-448.6C497.2,-434.96 475.99,-417.04 458.13,-401.95"/>
<polygon fill="#5c6380" stroke="#5c6380" points="460.72,-399.55 450.82,-395.77 456.2,-404.9 460.72,-399.55"/>
<text xml:space="preserve" text-anchor="middle" x="493.75" y="-417.94" font-family="DejaVu Sans" font-size="9.00">nein</text>
</g>
<!-- routing -->
<g id="node6" class="node">
<title>routing</title>
<path fill="#fffbeb" stroke="#b45309" stroke-width="2" d="M696.16,-390.09C696.16,-390.09 610.88,-390.09 610.88,-390.09 604.88,-390.09 598.88,-384.09 598.88,-378.09 598.88,-378.09 598.88,-363.63 598.88,-363.63 598.88,-357.63 604.88,-351.63 610.88,-351.63 610.88,-351.63 696.16,-351.63 696.16,-351.63 702.16,-351.63 708.16,-357.63 708.16,-363.63 708.16,-363.63 708.16,-378.09 708.16,-378.09 708.16,-384.09 702.16,-390.09 696.16,-390.09"/>
<text xml:space="preserve" text-anchor="middle" x="653.52" y="-374.11" font-family="DejaVu Sans" font-size="10.00">OR&#45;Tools Routing</text>
<text xml:space="preserve" text-anchor="middle" x="653.52" y="-361.36" font-family="DejaVu Sans" font-size="10.00">(Kapitel Graphen)</text>
</g>
<!-- f1&#45;&gt;routing -->
<g id="edge1" class="edge">
<title>f1&#45;&gt;routing</title>
<path fill="none" stroke="#5c6380" d="M563.47,-448.6C580.84,-433.8 604.13,-413.96 622.66,-398.16"/>
<polygon fill="#5c6380" stroke="#5c6380" points="624.84,-400.9 630.18,-391.75 620.3,-395.57 624.84,-400.9"/>
<text xml:space="preserve" text-anchor="middle" x="603.22" y="-417.94" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- f3 -->
<g id="node3" class="node">
<title>f3</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="264.52,-286.48 122.5,-258.1 264.52,-229.72 406.54,-258.1 264.52,-286.48"/>
<text xml:space="preserve" text-anchor="middle" x="264.52" y="-261.35" font-family="DejaVu Sans" font-size="10.00">Zielfunktion und alle</text>
<text xml:space="preserve" text-anchor="middle" x="264.52" y="-248.6" font-family="DejaVu Sans" font-size="10.00">Nebenbedingungen linear?</text>
</g>
<!-- f2&#45;&gt;f3 -->
<g id="edge4" class="edge">
<title>f2&#45;&gt;f3</title>
<path fill="none" stroke="#5c6380" d="M390.93,-347.71C366.19,-330.38 331.63,-306.14 304.89,-287.4"/>
<polygon fill="#5c6380" stroke="#5c6380" points="307.23,-284.76 297.03,-281.89 303.21,-290.5 307.23,-284.76"/>
<text xml:space="preserve" text-anchor="middle" x="367.42" y="-317.93" font-family="DejaVu Sans" font-size="9.00">nein</text>
</g>
<!-- f4 -->
<g id="node4" class="node">
<title>f4</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="581.52,-299.23 431.25,-258.1 581.52,-216.97 731.79,-258.1 581.52,-299.23"/>
<text xml:space="preserve" text-anchor="middle" x="581.52" y="-267.73" font-family="DejaVu Sans" font-size="10.00">Logische Regeln</text>
<text xml:space="preserve" text-anchor="middle" x="581.52" y="-254.98" font-family="DejaVu Sans" font-size="10.00">(wenn&#45;dann, entweder&#45;oder)</text>
<text xml:space="preserve" text-anchor="middle" x="581.52" y="-242.23" font-family="DejaVu Sans" font-size="10.00">oder Zeitplanung?</text>
</g>
<!-- f2&#45;&gt;f4 -->
<g id="edge3" class="edge">
<title>f2&#45;&gt;f4</title>
<path fill="none" stroke="#5c6380" d="M454.31,-347.71C476.01,-332.6 505.23,-312.24 530.19,-294.85"/>
<polygon fill="#5c6380" stroke="#5c6380" points="532.08,-297.8 538.29,-289.22 528.08,-292.06 532.08,-297.8"/>
<text xml:space="preserve" text-anchor="middle" x="501" y="-317.93" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- f5 -->
<g id="node5" class="node">
<title>f5</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="134.52,-151.22 0,-122.84 134.52,-94.46 269.04,-122.84 134.52,-151.22"/>
<text xml:space="preserve" text-anchor="middle" x="134.52" y="-126.09" font-family="DejaVu Sans" font-size="10.00">Nur Fluss oder</text>
<text xml:space="preserve" text-anchor="middle" x="134.52" y="-113.34" font-family="DejaVu Sans" font-size="10.00">Zuordnung im Netzwerk?</text>
</g>
<!-- f3&#45;&gt;f5 -->
<g id="edge7" class="edge">
<title>f3&#45;&gt;f5</title>
<path fill="none" stroke="#5c6380" d="M241.58,-233.58C220.32,-211.79 188.55,-179.22 165.14,-155.22"/>
<polygon fill="#5c6380" stroke="#5c6380" points="167.78,-152.92 158.29,-148.21 162.77,-157.81 167.78,-152.92"/>
<text xml:space="preserve" text-anchor="middle" x="211.05" y="-181.17" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- teil3 -->
<g id="node11" class="node">
<title>teil3</title>
<path fill="#fffbeb" stroke="#b45309" stroke-width="2" d="M379.16,-140.84C379.16,-140.84 305.88,-140.84 305.88,-140.84 299.88,-140.84 293.88,-134.84 293.88,-128.84 293.88,-128.84 293.88,-116.84 293.88,-116.84 293.88,-110.84 299.88,-104.84 305.88,-104.84 305.88,-104.84 379.16,-104.84 379.16,-104.84 385.16,-104.84 391.16,-110.84 391.16,-116.84 391.16,-116.84 391.16,-128.84 391.16,-128.84 391.16,-134.84 385.16,-140.84 379.16,-140.84"/>
<text xml:space="preserve" text-anchor="middle" x="342.52" y="-119.72" font-family="DejaVu Sans" font-size="10.00">weiter in Teil III</text>
</g>
<!-- f3&#45;&gt;teil3 -->
<g id="edge8" class="edge">
<title>f3&#45;&gt;teil3</title>
<path fill="none" stroke="#5c6380" d="M279.19,-232.03C292.65,-209.05 312.53,-175.07 326.42,-151.35"/>
<polygon fill="#5c6380" stroke="#5c6380" points="329.36,-153.26 331.39,-142.86 323.31,-149.72 329.36,-153.26"/>
<text xml:space="preserve" text-anchor="middle" x="324.81" y="-181.17" font-family="DejaVu Sans" font-size="9.00">nein</text>
</g>
<!-- cpsat -->
<g id="node7" class="node">
<title>cpsat</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M532.16,-142.07C532.16,-142.07 452.88,-142.07 452.88,-142.07 446.88,-142.07 440.88,-136.07 440.88,-130.07 440.88,-130.07 440.88,-115.61 440.88,-115.61 440.88,-109.61 446.88,-103.61 452.88,-103.61 452.88,-103.61 532.16,-103.61 532.16,-103.61 538.16,-103.61 544.16,-109.61 544.16,-115.61 544.16,-115.61 544.16,-130.07 544.16,-130.07 544.16,-136.07 538.16,-142.07 532.16,-142.07"/>
<text xml:space="preserve" text-anchor="middle" x="492.52" y="-126.09" font-family="DejaVu Sans" font-size="10.00">CP&#45;SAT</text>
<text xml:space="preserve" text-anchor="middle" x="492.52" y="-113.34" font-family="DejaVu Sans" font-size="10.00">(Kapitel CP&#45;SAT)</text>
</g>
<!-- f4&#45;&gt;cpsat -->
<g id="edge5" class="edge">
<title>f4&#45;&gt;cpsat</title>
<path fill="none" stroke="#5c6380" d="M558.6,-222.78C543.99,-200.91 525.3,-172.91 511.52,-152.29"/>
<polygon fill="#5c6380" stroke="#5c6380" points="514.5,-150.44 506.03,-144.07 508.67,-154.33 514.5,-150.44"/>
<text xml:space="preserve" text-anchor="middle" x="546.1" y="-181.17" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- milp -->
<g id="node8" class="node">
<title>milp</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M685.54,-142.07C685.54,-142.07 581.5,-142.07 581.5,-142.07 575.5,-142.07 569.5,-136.07 569.5,-130.07 569.5,-130.07 569.5,-115.61 569.5,-115.61 569.5,-109.61 575.5,-103.61 581.5,-103.61 581.5,-103.61 685.54,-103.61 685.54,-103.61 691.54,-103.61 697.54,-109.61 697.54,-115.61 697.54,-115.61 697.54,-130.07 697.54,-130.07 697.54,-136.07 691.54,-142.07 685.54,-142.07"/>
<text xml:space="preserve" text-anchor="middle" x="633.52" y="-126.09" font-family="DejaVu Sans" font-size="10.00">MILP: HiGHS / CP&#45;SAT</text>
<text xml:space="preserve" text-anchor="middle" x="633.52" y="-113.34" font-family="DejaVu Sans" font-size="10.00">(Kapitel MILP)</text>
</g>
<!-- f4&#45;&gt;milp -->
<g id="edge6" class="edge">
<title>f4&#45;&gt;milp</title>
<path fill="none" stroke="#5c6380" d="M595.87,-220.33C604.1,-199.24 614.29,-173.11 622,-153.37"/>
<polygon fill="#5c6380" stroke="#5c6380" points="625.18,-154.85 625.55,-144.26 618.66,-152.3 625.18,-154.85"/>
<text xml:space="preserve" text-anchor="middle" x="642.97" y="-192.42" font-family="DejaVu Sans" font-size="9.00">nein</text>
<text xml:space="preserve" text-anchor="middle" x="642.97" y="-181.17" font-family="DejaVu Sans" font-size="9.00">(Fixkosten,</text>
<text xml:space="preserve" text-anchor="middle" x="642.97" y="-169.92" font-family="DejaVu Sans" font-size="9.00">Kardinalität)</text>
</g>
<!-- netz -->
<g id="node9" class="node">
<title>netz</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M116.29,-51.21C116.29,-51.21 16.75,-51.21 16.75,-51.21 10.75,-51.21 4.75,-45.21 4.75,-39.21 4.75,-39.21 4.75,-12 4.75,-12 4.75,-6 10.75,0 16.75,0 16.75,0 116.29,0 116.29,0 122.29,0 128.29,-6 128.29,-12 128.29,-12 128.29,-39.21 128.29,-39.21 128.29,-45.21 122.29,-51.21 116.29,-51.21"/>
<text xml:space="preserve" text-anchor="middle" x="66.52" y="-35.23" font-family="DejaVu Sans" font-size="10.00">Spezialalgorithmus:</text>
<text xml:space="preserve" text-anchor="middle" x="66.52" y="-22.48" font-family="DejaVu Sans" font-size="10.00">Min&#45;Cost&#45;Flow,</text>
<text xml:space="preserve" text-anchor="middle" x="66.52" y="-9.73" font-family="DejaVu Sans" font-size="10.00">Ungarische Methode</text>
</g>
<!-- f5&#45;&gt;netz -->
<g id="edge9" class="edge">
<title>f5&#45;&gt;netz</title>
<path fill="none" stroke="#5c6380" d="M117.36,-97.81C109.4,-86.66 99.78,-73.19 91.07,-60.99"/>
<polygon fill="#5c6380" stroke="#5c6380" points="94.16,-59.3 85.51,-53.19 88.47,-63.37 94.16,-59.3"/>
<text xml:space="preserve" text-anchor="middle" x="106.31" y="-69.91" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- lp -->
<g id="node10" class="node">
<title>lp</title>
<path fill="#eef2ff" stroke="#4338ca" stroke-width="2" d="M242.04,-51.21C242.04,-51.21 165,-51.21 165,-51.21 159,-51.21 153,-45.21 153,-39.21 153,-39.21 153,-12 153,-12 153,-6 159,0 165,0 165,0 242.04,0 242.04,0 248.04,0 254.04,-6 254.04,-12 254.04,-12 254.04,-39.21 254.04,-39.21 254.04,-45.21 248.04,-51.21 242.04,-51.21"/>
<text xml:space="preserve" text-anchor="middle" x="203.52" y="-35.23" font-family="DejaVu Sans" font-size="10.00">LP: scipy.linprog</text>
<text xml:space="preserve" text-anchor="middle" x="203.52" y="-22.48" font-family="DejaVu Sans" font-size="10.00">oder highspy</text>
<text xml:space="preserve" text-anchor="middle" x="203.52" y="-9.73" font-family="DejaVu Sans" font-size="10.00">(Kapitel LP)</text>
</g>
<!-- f5&#45;&gt;lp -->
<g id="edge10" class="edge">
<title>f5&#45;&gt;lp</title>
<path fill="none" stroke="#5c6380" d="M151.93,-97.81C160.01,-86.66 169.77,-73.19 178.61,-60.99"/>
<polygon fill="#5c6380" stroke="#5c6380" points="181.23,-63.34 184.26,-53.19 175.56,-59.23 181.23,-63.34"/>
<text xml:space="preserve" text-anchor="middle" x="182.9" y="-69.91" font-family="DejaVu Sans" font-size="9.00">nein</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View file

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (20260126.1125)
-->
<!-- Pages: 1 -->
<svg width="990pt" height="408pt"
viewBox="0.00 0.00 990.00 408.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 403.74)">
<text xml:space="preserve" text-anchor="middle" x="490.89" y="-383.39" font-family="DejaVu Sans" font-size="13.00">Welches Werkzeug passt zu meinem Problem? (Teil III)</text>
<!-- g1 -->
<g id="node1" class="node">
<title>g1</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="510.89,-375.24 395.88,-346.86 510.89,-318.48 625.91,-346.86 510.89,-375.24"/>
<text xml:space="preserve" text-anchor="middle" x="510.89" y="-350.11" font-family="DejaVu Sans" font-size="10.00">Sind die Daten sicher</text>
<text xml:space="preserve" text-anchor="middle" x="510.89" y="-337.36" font-family="DejaVu Sans" font-size="10.00">bekannt?</text>
</g>
<!-- g2 -->
<g id="node2" class="node">
<title>g2</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="370.89,-275.23 259.62,-234.1 370.89,-192.97 482.16,-234.1 370.89,-275.23"/>
<text xml:space="preserve" text-anchor="middle" x="370.89" y="-243.72" font-family="DejaVu Sans" font-size="10.00">Entscheidung fällt in</text>
<text xml:space="preserve" text-anchor="middle" x="370.89" y="-230.97" font-family="DejaVu Sans" font-size="10.00">mehreren Stufen</text>
<text xml:space="preserve" text-anchor="middle" x="370.89" y="-218.22" font-family="DejaVu Sans" font-size="10.00">über die Zeit?</text>
</g>
<!-- g1&#45;&gt;g2 -->
<g id="edge2" class="edge">
<title>g1&#45;&gt;g2</title>
<path fill="none" stroke="#5c6380" d="M483.9,-324.5C464.33,-309.02 437.44,-287.74 414.8,-269.83"/>
<polygon fill="#5c6380" stroke="#5c6380" points="417.23,-267.3 407.22,-263.84 412.89,-272.79 417.23,-267.3"/>
<text xml:space="preserve" text-anchor="middle" x="463.18" y="-293.93" font-family="DejaVu Sans" font-size="9.00">nein</text>
</g>
<!-- g4 -->
<g id="node4" class="node">
<title>g4</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="651.89,-275.23 506.88,-234.1 651.89,-192.97 796.91,-234.1 651.89,-275.23"/>
<text xml:space="preserve" text-anchor="middle" x="651.89" y="-243.72" font-family="DejaVu Sans" font-size="10.00">Zielfunktion konvex?</text>
<text xml:space="preserve" text-anchor="middle" x="651.89" y="-230.97" font-family="DejaVu Sans" font-size="10.00">(Quadrate, Normen,</text>
<text xml:space="preserve" text-anchor="middle" x="651.89" y="-218.22" font-family="DejaVu Sans" font-size="10.00">log/exp konvex kombiniert)</text>
</g>
<!-- g1&#45;&gt;g4 -->
<g id="edge1" class="edge">
<title>g1&#45;&gt;g4</title>
<path fill="none" stroke="#5c6380" d="M538.08,-324.5C556.93,-309.7 582.53,-289.59 604.67,-272.2"/>
<polygon fill="#5c6380" stroke="#5c6380" points="606.71,-275.05 612.41,-266.12 602.38,-269.54 606.71,-275.05"/>
<text xml:space="preserve" text-anchor="middle" x="580.92" y="-293.93" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- g3 -->
<g id="node3" class="node">
<title>g3</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="155.89,-149.72 40.88,-121.34 155.89,-92.96 270.91,-121.34 155.89,-149.72"/>
<text xml:space="preserve" text-anchor="middle" x="155.89" y="-124.59" font-family="DejaVu Sans" font-size="10.00">Wahrscheinlichkeiten</text>
<text xml:space="preserve" text-anchor="middle" x="155.89" y="-111.84" font-family="DejaVu Sans" font-size="10.00">bekannt?</text>
</g>
<!-- g2&#45;&gt;g3 -->
<g id="edge4" class="edge">
<title>g2&#45;&gt;g3</title>
<path fill="none" stroke="#5c6380" d="M324.77,-209.34C288.68,-190.75 238.71,-165 202.5,-146.35"/>
<polygon fill="#5c6380" stroke="#5c6380" points="204.51,-143.45 194.02,-141.98 201.3,-149.67 204.51,-143.45"/>
<text xml:space="preserve" text-anchor="middle" x="266.7" y="-168.42" font-family="DejaVu Sans" font-size="9.00">nein</text>
</g>
<!-- dp -->
<g id="node6" class="node">
<title>dp</title>
<path fill="#fffbeb" stroke="#b45309" stroke-width="2" d="M452.16,-140.57C452.16,-140.57 307.62,-140.57 307.62,-140.57 301.62,-140.57 295.62,-134.57 295.62,-128.57 295.62,-128.57 295.62,-114.11 295.62,-114.11 295.62,-108.11 301.62,-102.11 307.62,-102.11 307.62,-102.11 452.16,-102.11 452.16,-102.11 458.16,-102.11 464.16,-108.11 464.16,-114.11 464.16,-114.11 464.16,-128.57 464.16,-128.57 464.16,-134.57 458.16,-140.57 452.16,-140.57"/>
<text xml:space="preserve" text-anchor="middle" x="379.89" y="-124.59" font-family="DejaVu Sans" font-size="10.00">Dynamische Programmierung</text>
<text xml:space="preserve" text-anchor="middle" x="379.89" y="-111.84" font-family="DejaVu Sans" font-size="10.00">(Bellman)</text>
</g>
<!-- g2&#45;&gt;dp -->
<g id="edge3" class="edge">
<title>g2&#45;&gt;dp</title>
<path fill="none" stroke="#5c6380" d="M374.12,-193.43C375.21,-179.97 376.41,-165.17 377.44,-152.57"/>
<polygon fill="#5c6380" stroke="#5c6380" points="380.91,-153.05 378.23,-142.8 373.93,-152.48 380.91,-153.05"/>
<text xml:space="preserve" text-anchor="middle" x="379.95" y="-168.42" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- stoch -->
<g id="node7" class="node">
<title>stoch</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M137.79,-38.46C137.79,-38.46 12,-38.46 12,-38.46 6,-38.46 0,-32.46 0,-26.46 0,-26.46 0,-12 0,-12 0,-6 6,0 12,0 12,0 137.79,0 137.79,0 143.79,0 149.79,-6 149.79,-12 149.79,-12 149.79,-26.46 149.79,-26.46 149.79,-32.46 143.79,-38.46 137.79,-38.46"/>
<text xml:space="preserve" text-anchor="middle" x="74.89" y="-22.48" font-family="DejaVu Sans" font-size="10.00">Zweistufige stochastische</text>
<text xml:space="preserve" text-anchor="middle" x="74.89" y="-9.73" font-family="DejaVu Sans" font-size="10.00">Optimierung, Szenarien</text>
</g>
<!-- g3&#45;&gt;stoch -->
<g id="edge5" class="edge">
<title>g3&#45;&gt;stoch</title>
<path fill="none" stroke="#5c6380" d="M136.9,-96.87C125.02,-82.18 109.66,-63.19 97.2,-47.79"/>
<polygon fill="#5c6380" stroke="#5c6380" points="100.21,-45.95 91.2,-40.38 94.76,-50.35 100.21,-45.95"/>
<text xml:space="preserve" text-anchor="middle" x="123.99" y="-62.79" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- robust -->
<g id="node8" class="node">
<title>robust</title>
<path fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" d="M289.16,-38.46C289.16,-38.46 186.62,-38.46 186.62,-38.46 180.62,-38.46 174.62,-32.46 174.62,-26.46 174.62,-26.46 174.62,-12 174.62,-12 174.62,-6 180.62,0 186.62,0 186.62,0 289.16,0 289.16,0 295.16,0 301.16,-6 301.16,-12 301.16,-12 301.16,-26.46 301.16,-26.46 301.16,-32.46 295.16,-38.46 289.16,-38.46"/>
<text xml:space="preserve" text-anchor="middle" x="237.89" y="-22.48" font-family="DejaVu Sans" font-size="10.00">Robuste Optimierung</text>
<text xml:space="preserve" text-anchor="middle" x="237.89" y="-9.73" font-family="DejaVu Sans" font-size="10.00">(Worst Case)</text>
</g>
<!-- g3&#45;&gt;robust -->
<g id="edge6" class="edge">
<title>g3&#45;&gt;robust</title>
<path fill="none" stroke="#5c6380" d="M174.91,-97.12C186.94,-82.44 202.54,-63.39 215.21,-47.93"/>
<polygon fill="#5c6380" stroke="#5c6380" points="217.69,-50.42 221.32,-40.47 212.28,-45.98 217.69,-50.42"/>
<text xml:space="preserve" text-anchor="middle" x="246.16" y="-68.41" font-family="DejaVu Sans" font-size="9.00">nein,</text>
<text xml:space="preserve" text-anchor="middle" x="246.16" y="-57.16" font-family="DejaVu Sans" font-size="9.00">nur Bandbreiten</text>
</g>
<!-- g5 -->
<g id="node5" class="node">
<title>g5</title>
<polygon fill="#eef2ff" stroke="#4338ca" stroke-width="1.6" points="646.89,-149.72 492.88,-121.34 646.89,-92.96 800.91,-121.34 646.89,-149.72"/>
<text xml:space="preserve" text-anchor="middle" x="646.89" y="-124.59" font-family="DejaVu Sans" font-size="10.00">Quadratisch mit</text>
<text xml:space="preserve" text-anchor="middle" x="646.89" y="-111.84" font-family="DejaVu Sans" font-size="10.00">linearen Nebenbedingungen?</text>
</g>
<!-- g4&#45;&gt;g5 -->
<g id="edge7" class="edge">
<title>g4&#45;&gt;g5</title>
<path fill="none" stroke="#5c6380" d="M650.08,-192.8C649.61,-182.57 649.12,-171.61 648.66,-161.44"/>
<polygon fill="#5c6380" stroke="#5c6380" points="652.16,-161.32 648.21,-151.48 645.17,-161.63 652.16,-161.32"/>
<text xml:space="preserve" text-anchor="middle" x="653" y="-168.42" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- nichtkonvex -->
<g id="node11" class="node">
<title>nichtkonvex</title>
<path fill="#fffbeb" stroke="#b45309" stroke-width="2" d="M969.79,-146.94C969.79,-146.94 838,-146.94 838,-146.94 832,-146.94 826,-140.94 826,-134.94 826,-134.94 826,-107.73 826,-107.73 826,-101.73 832,-95.73 838,-95.73 838,-95.73 969.79,-95.73 969.79,-95.73 975.79,-95.73 981.79,-101.73 981.79,-107.73 981.79,-107.73 981.79,-134.94 981.79,-134.94 981.79,-140.94 975.79,-146.94 969.79,-146.94"/>
<text xml:space="preserve" text-anchor="middle" x="903.89" y="-130.97" font-family="DejaVu Sans" font-size="10.00">scipy.optimize.minimize</text>
<text xml:space="preserve" text-anchor="middle" x="903.89" y="-118.22" font-family="DejaVu Sans" font-size="10.00">mit mehreren Startpunkten</text>
<text xml:space="preserve" text-anchor="middle" x="903.89" y="-105.47" font-family="DejaVu Sans" font-size="10.00">— nur lokales Optimum!</text>
</g>
<!-- g4&#45;&gt;nichtkonvex -->
<g id="edge8" class="edge">
<title>g4&#45;&gt;nichtkonvex</title>
<path fill="none" stroke="#5c6380" d="M708.45,-208.24C746.01,-191.73 795.56,-169.96 835.36,-152.46"/>
<polygon fill="#5c6380" stroke="#5c6380" points="836.65,-155.72 844.4,-148.49 833.83,-149.31 836.65,-155.72"/>
<text xml:space="preserve" text-anchor="middle" x="810.22" y="-168.42" font-family="DejaVu Sans" font-size="9.00">nein</text>
</g>
<!-- qp -->
<g id="node9" class="node">
<title>qp</title>
<path fill="#ecfeff" stroke="#0891b2" stroke-width="2" d="M611.91,-38.46C611.91,-38.46 531.88,-38.46 531.88,-38.46 525.88,-38.46 519.88,-32.46 519.88,-26.46 519.88,-26.46 519.88,-12 519.88,-12 519.88,-6 525.88,0 531.88,0 531.88,0 611.91,0 611.91,0 617.91,0 623.91,-6 623.91,-12 623.91,-12 623.91,-26.46 623.91,-26.46 623.91,-32.46 617.91,-38.46 611.91,-38.46"/>
<text xml:space="preserve" text-anchor="middle" x="571.89" y="-22.48" font-family="DejaVu Sans" font-size="10.00">QP über CVXPY</text>
<text xml:space="preserve" text-anchor="middle" x="571.89" y="-9.73" font-family="DejaVu Sans" font-size="10.00">(OSQP, Clarabel)</text>
</g>
<!-- g5&#45;&gt;qp -->
<g id="edge9" class="edge">
<title>g5&#45;&gt;qp</title>
<path fill="none" stroke="#5c6380" d="M628.36,-95.59C617.68,-81.34 604.22,-63.38 593.13,-48.57"/>
<polygon fill="#5c6380" stroke="#5c6380" points="595.93,-46.47 587.13,-40.57 590.33,-50.67 595.93,-46.47"/>
<text xml:space="preserve" text-anchor="middle" x="617.64" y="-62.79" font-family="DejaVu Sans" font-size="9.00">ja</text>
</g>
<!-- konvex -->
<g id="node10" class="node">
<title>konvex</title>
<path fill="#ecfdf5" stroke="#059669" stroke-width="2" d="M782.91,-38.46C782.91,-38.46 660.88,-38.46 660.88,-38.46 654.88,-38.46 648.88,-32.46 648.88,-26.46 648.88,-26.46 648.88,-12 648.88,-12 648.88,-6 654.88,0 660.88,0 660.88,0 782.91,0 782.91,0 788.91,0 794.91,-6 794.91,-12 794.91,-12 794.91,-26.46 794.91,-26.46 794.91,-32.46 788.91,-38.46 782.91,-38.46"/>
<text xml:space="preserve" text-anchor="middle" x="721.89" y="-22.48" font-family="DejaVu Sans" font-size="10.00">CVXPY</text>
<text xml:space="preserve" text-anchor="middle" x="721.89" y="-9.73" font-family="DejaVu Sans" font-size="10.00">— prüft Konvexität selbst</text>
</g>
<!-- g5&#45;&gt;konvex -->
<g id="edge10" class="edge">
<title>g5&#45;&gt;konvex</title>
<path fill="none" stroke="#5c6380" d="M665.43,-95.59C676.11,-81.34 689.57,-63.38 700.66,-48.57"/>
<polygon fill="#5c6380" stroke="#5c6380" points="703.46,-50.67 706.66,-40.57 697.86,-46.47 703.46,-50.67"/>
<text xml:space="preserve" text-anchor="middle" x="705.25" y="-62.79" font-family="DejaVu Sans" font-size="9.00">nein</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

BIN
bilder_04/titelseite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

2812
bilder_04/titelseite.svg Normal file

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 98 KiB