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,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)