Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
# erzeuge_min_cost_flow.py
|
|
|
|
|
"""
|
|
|
|
|
Erzeugt das Flussdiagramm zum Graphen-Kapitel:
|
|
|
|
|
|
Aufgeraeumt: 33 PNG-Zweitfassungen, Bau-Ueberbleibsel, Ausgabepfade
Drei Aufraeumarbeiten - und zwei Funde, die dabei auffielen.
1. DIE PNG-ZWEITFASSUNGEN SIND WEG. Jedes Diagramm lag doppelt vor, als SVG
und als PNG, und kein einziges src=/href= in der Website zeigte je auf ein
PNG. Der Build kopierte sie trotzdem mit: 3,3 MB im Repository plus 3,3 MB,
die bei jeder Veroeffentlichung auf den Webserver gingen. Die 15 Generatoren
schreiben jetzt nur noch SVG, die Docstrings sind mitgezogen. Vor dem
Loeschen geprueft: Jedes PNG hatte sein gleichnamiges SVG, alle 33 waren
versioniert.
FUND 1: erzeuge_kap06_gantt.py folgte als einziger Generator nicht der
Konvention - weder svg.hashsalt noch metadata={"Date": None}. Sein SVG trug
einen echten Zeitstempel und bei jedem Lauf andere clip-path-IDs, war also
nie byteidentisch reproduzierbar, obwohl CLAUDE.md genau das fuer alle
Generatoren festhaelt. Aufgefallen nur, weil nach der PNG-Umstellung 32 von
33 SVGs bitgleich blieben und eines nicht. Jetzt byteidentisch ueber zwei
Laeufe.
FUND 2: spiegle_bilder() legte leere Verzeichnisse auf dem Webserver an. Der
Dateifilter arbeitete korrekt, aber os.walk durchlief auch __pycache__/, und
os.makedirs() erzeugte es am Ziel. Die Verzeichnisliste wird jetzt vorher
gefiltert. Gegengetestet.
2. BAU-UEBERBLEIBSEL entfernt (alle ignoriert und neu erzeugbar):
svg-inkscape/, build_v04.log, .pytest_cache/, Programme/output/, vier
__pycache__/ und die Excel-Mappen. Arbeitsbaum 36 -> 33 MB, danach null
ignorierte Ueberbleibsel.
3. Excel_Bruecke.py SCHREIBT NEBEN DAS SKRIPT statt ins
Arbeitsverzeichnis. Es benutzte blanke relative Namen; wer es aus der
Repository-Wurzel startete, verstreute dort produktionsmix.xlsx und
produktionsmix_ergebnis.xlsx. Jetzt wie die vier anderen schreibenden
Programme ueber os.path.dirname(os.path.abspath(__file__)). Nachgemessen:
Lauf aus der Wurzel legt dort null Dateien ab.
Ausserdem git gc: 653 lose Objekte gepackt, .git von 71 MB auf 28 MB -
reines Repacken, kein Inhalt beruehrt.
Geprueft: alle 33 im Buch referenzierten SVGs vorhanden, in Quelle und
Website; 16 Generatoren fehlerfrei; keine fehlenden Bilder im LaTeX-Lauf;
33 pytest-Tests; PDF unveraendert 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:22:26 +02:00
|
|
|
bilder_04/kap_graphen_min_cost_flow.svg
|
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
|
|
|
|
|
|
|
|
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)
|
Aufgeraeumt: 33 PNG-Zweitfassungen, Bau-Ueberbleibsel, Ausgabepfade
Drei Aufraeumarbeiten - und zwei Funde, die dabei auffielen.
1. DIE PNG-ZWEITFASSUNGEN SIND WEG. Jedes Diagramm lag doppelt vor, als SVG
und als PNG, und kein einziges src=/href= in der Website zeigte je auf ein
PNG. Der Build kopierte sie trotzdem mit: 3,3 MB im Repository plus 3,3 MB,
die bei jeder Veroeffentlichung auf den Webserver gingen. Die 15 Generatoren
schreiben jetzt nur noch SVG, die Docstrings sind mitgezogen. Vor dem
Loeschen geprueft: Jedes PNG hatte sein gleichnamiges SVG, alle 33 waren
versioniert.
FUND 1: erzeuge_kap06_gantt.py folgte als einziger Generator nicht der
Konvention - weder svg.hashsalt noch metadata={"Date": None}. Sein SVG trug
einen echten Zeitstempel und bei jedem Lauf andere clip-path-IDs, war also
nie byteidentisch reproduzierbar, obwohl CLAUDE.md genau das fuer alle
Generatoren festhaelt. Aufgefallen nur, weil nach der PNG-Umstellung 32 von
33 SVGs bitgleich blieben und eines nicht. Jetzt byteidentisch ueber zwei
Laeufe.
FUND 2: spiegle_bilder() legte leere Verzeichnisse auf dem Webserver an. Der
Dateifilter arbeitete korrekt, aber os.walk durchlief auch __pycache__/, und
os.makedirs() erzeugte es am Ziel. Die Verzeichnisliste wird jetzt vorher
gefiltert. Gegengetestet.
2. BAU-UEBERBLEIBSEL entfernt (alle ignoriert und neu erzeugbar):
svg-inkscape/, build_v04.log, .pytest_cache/, Programme/output/, vier
__pycache__/ und die Excel-Mappen. Arbeitsbaum 36 -> 33 MB, danach null
ignorierte Ueberbleibsel.
3. Excel_Bruecke.py SCHREIBT NEBEN DAS SKRIPT statt ins
Arbeitsverzeichnis. Es benutzte blanke relative Namen; wer es aus der
Repository-Wurzel startete, verstreute dort produktionsmix.xlsx und
produktionsmix_ergebnis.xlsx. Jetzt wie die vier anderen schreibenden
Programme ueber os.path.dirname(os.path.abspath(__file__)). Nachgemessen:
Lauf aus der Wurzel legt dort null Dateien ab.
Ausserdem git gc: 653 lose Objekte gepackt, .git von 71 MB auf 28 MB -
reines Repacken, kein Inhalt beruehrt.
Geprueft: alle 33 im Buch referenzierten SVGs vorhanden, in Quelle und
Website; 16 Generatoren fehlerfrei; keine fehlenden Bilder im LaTeX-Lauf;
33 pytest-Tests; PDF unveraendert 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:22:26 +02:00
|
|
|
for endung in ("svg",):
|
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
|
|
|
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)
|