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