#!/usr/bin/env python3 # erzeuge_min_cost_flow.py """ Erzeugt das Flussdiagramm zum Graphen-Kapitel: bilder_04/kap_graphen_min_cost_flow.svg 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",): 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)