operations_research/bilder_04/erzeuge_big_m_vergleich.py

121 lines
5.1 KiB
Python
Raw Permalink Normal View History

Grafik-Upgrade Stufe 1: elf neue Diagramme Grafik_Upgrade.md verlangt "37 didaktisch wertvolle Visualisierungen". Es sind 19: Die Abschnitte "Modul 1-4" wiederholen die Grafiken 1-18 unter den Nummern 19-36, nur Grafik 37 ist zusaetzlich. Belegt an den Skriptnamen - 38 Nennungen, 19 verschiedene Dateien, erzeuge_kombinatorik_wand.py steht dreimal darin. Von den 19 betreffen neun bereits vorhandene Diagramme. Umgesetzt sind die zehn wirklich neuen plus eine elfte, die aus einem Sachfehler des Auftrags entstand: Grafik 14 sollte in Abschnitt 12.3 stehen, nannte aber Zahlen aus Kapitel 17. Beide Stellen haben jetzt ihre eigene Grafik mit ihren eigenen Zahlen. Vier Vorgaben des Auftrags wurden bewusst nicht befolgt: PNG bei 300 dpi (das Buch kennt nur SVG - die 33 verwaisten PNGs waren gerade erst geloescht worden), Matplotlib-Standardfarben (die Buchpalette steht in 33 Diagrammen), harte Kapitelnummern im Fliesstext ({ref:sec:...} stattdessen) und die fehlende Byte-Reproduzierbarkeit. Der eigentliche Ertrag waren die Selbsttests. Jeder Generator prueft seine Zahlen gegen das, was im Kapitel steht, und bricht bei Abweichung ab. Vier haben angeschlagen: * Kombinatorik-Wand: 25! ergibt 491,5 Mio. Jahre, die Tabelle nennt 490 Mio. - Rundung auf zwei signifikante Stellen, nicht Fehler des Buchs. * Laufzeit-Diagramm: Die abgedruckte Prozentspalte laesst sich aus den abgedruckten Zeiten nicht exakt nachrechnen (0,001/0,008 ergibt 12 %, gedruckt sind 14 %) - die Zeiten sind gerundet, die Prozente nicht. * Predict-then-Optimize: geratene Datenaufteilung ergab 197,35 statt 200,95 MSE. Mit TRAINING aus dem Buchprogramm stimmen alle vier Kostenwerte. * Almgren-Chriss: ein echter Fehler im Buchprogramm. DER FUND: analytische_loesung() minimiert die eigene Kostenfunktion nicht. Ihr Pfad kostet 10.860,40 EUR, der DP-Pfad 10.266,24 EUR - ein Gitterverfahren kann das kontinuierliche Optimum aber nicht unterbieten. periodenkosten() rechnet das Risiko mit lambda/2, die geschlossene Formel setzt kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 liefert die Formel 10.264,85 EUR und denselben Pfad wie die DP, knapp UNTER der Gitterloesung. Die Grafik zeigt bis zur Entscheidung darueber den DP-Pfad; er ist der, den das Kapitel abdruckt. Nachweise: Alle elf Diagramme sind ueber zwei Laeufe byte-identisch. Kein vorhandenes Diagramm wurde veraendert. Die Lastabwurf-Grafik reproduziert die Befundtabelle exakt (874.870/2.504.154/28 von 40 gegen 351.356/415.924/0). Zwei Abbildungen landeten beim automatischen Einfuegen im Codeblock (ihre Anker standen in abgedruckten Ausgaben) und wurden dahinter verschoben; eine Pruefung ueber alle elf zeigt null Fehlplatzierungen. Neu: bilder_04/stil_04.py buendelt Palette, rcParams und speichere(). networkx wurde entgegen der Planung nicht gebraucht - pyproject.toml bleibt unveraendert. PDF von 781 auf 788 Seiten, 44 Diagramme, 27 Generatoren. Veroeffentlicht: 25 Dateien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 01:11:33 +02:00
#!/usr/bin/env python3
# erzeuge_big_m_vergleich.py
"""
Erzeugt das Big-M-Diagramm zum MILP-Kapitel:
bilder_04/kap_milp_big_m.svg
Zwei Bilder derselben Bedingung x <= M*y mit y in [0,1], einmal mit straffem
und einmal mit ueberdimensioniertem M. Gezeigt wird die LP-RELAXATION - also
das, was der Solver sieht, bevor er y auf 0 oder 1 zwingt.
Der Punkt: Beide Modelle sind fuer y in {0,1} GLEICHWERTIG. Sie beschreiben
denselben zulaessigen Bereich, sobald y ganzzahlig ist. Verschieden ist nur die
Relaxation - und die entscheidet, wie gut die Schranke ist und wie lange der
Solver sucht. Bei zu grossem M kommt der Trickle Flow dazu: Ein y knapp ueber
null gilt dem Solver als geschlossen und laesst trotzdem grossen Fluss durch.
Die Trickle-Flow-Zahlen in der Anmerkung stammen aus der Loesung im Anhang des
Buchs (M = 7,2e9, y = 4e-8, rund 289 Einheiten Fluss). Die beiden M-Werte der
Geometrie sind Anschauungswerte; das Skript rechnet aus, ab welchem y die
Relaxation die volle Kapazitaet freigibt, statt eine Zahl zu behaupten. Die
Flaeche des Relaxationsgebiets waere der falsche Massstab - sie ist wegen der
Kapazitaetsgrenze in beiden Faellen aehnlich gross.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_big_m_vergleich.py
Benoetigt: numpy, matplotlib
"""
from __future__ import annotations
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from stil_04 import FARBEN, speichere # noqa: E402
import matplotlib.pyplot as plt # noqa: E402
KAPAZITAET = 40.0 # der tatsaechlich moegliche Hoechstwert von x
M_STRAFF = 40.0 # so klein wie moeglich: M = Kapazitaet
M_GROSS = 500.0 # "sicherheitshalber gross" - der haeufige Fehler
# Aus der Loesung im Anhang: ein verschwindend kleines y laesst grossen Fluss
# durch, weil es mit einem riesigen M multipliziert wird.
TRICKLE_M = 7.2e9
TRICKLE_Y = 4e-8
def schwelle(m: float) -> float:
"""Kleinstes y, bei dem die Relaxation die volle Kapazitaet freigibt.
Das ist der aussagekraeftige Vergleich - nicht die Flaeche des
Relaxationsgebiets. Die ist wegen der Kapazitaetsgrenze in beiden Faellen
aehnlich gross; der Unterschied liegt darin, wie WENIG y genuegt, um alles
durchzulassen."""
return min(1.0, KAPAZITAET / m)
def zeichne(faktor: float) -> None:
figur, achsen = plt.subplots(1, 2, figsize=(9.2, 4.0), sharey=True)
y = np.linspace(0, 1, 300)
for achse, m, titel, farbe in (
(achsen[0], M_STRAFF, f"straff: $M = {M_STRAFF:.0f}$",
FARBEN["gut"]),
(achsen[1], M_GROSS, f"zu groß: $M = {M_GROSS:.0f}$",
FARBEN["fehler"])):
obergrenze = np.minimum(m * y, KAPAZITAET)
achse.fill_between(y, 0, obergrenze, color=farbe, alpha=0.16, zorder=2)
achse.plot(y, obergrenze, "-", color=farbe, linewidth=2.2, zorder=4,
label=r"$x \le M\,y$ und $x \le$ Kapazität")
achse.axhline(KAPAZITAET, color=FARBEN["linie"], linewidth=1.0,
linestyle="--", zorder=3)
achse.text(0.02, KAPAZITAET + 1.5, "Kapazität", fontsize=8.5,
color=FARBEN["gedaempft"])
# Die beiden Punkte, die das ganzzahlige Modell zulaesst.
achse.plot([0, 1], [0, KAPAZITAET], "o", color=FARBEN["haupt"],
markersize=8, zorder=5)
achse.annotate(r"$y=0$: zu", xy=(0, 0), xytext=(8, 10),
textcoords="offset points", fontsize=8.5,
color=FARBEN["haupt"])
achse.annotate(r"$y=1$: offen", xy=(1, KAPAZITAET), xytext=(-64, -18),
textcoords="offset points", fontsize=8.5,
color=FARBEN["haupt"])
achse.set_xlim(0, 1.02)
achse.set_ylim(0, KAPAZITAET * 1.25)
achse.set_xlabel(r"Schaltvariable $y \in [0,1]$ (relaxiert)")
achse.set_title(titel, fontsize=10.5, color=farbe)
achsen[0].set_ylabel(r"Menge $x$")
achsen[1].annotate(
f"Schon $y = {schwelle(M_GROSS):.2f}$ gibt die volle\n"
f"Kapazität frei — links braucht es dafür $y = 1$.\n"
f"Der Solver darf das Lager also fast\n"
f"geschlossen lassen und trotzdem alles liefern.",
xy=(0.16, KAPAZITAET * 0.42), fontsize=9, color=FARBEN["fehler"],
ha="left")
achsen[1].plot([schwelle(M_GROSS)], [KAPAZITAET], "o",
color=FARBEN["fehler"], markersize=8, zorder=6)
figur.suptitle("Dieselbe Bedingung, zwei Zahlen für $M$ — gleich für "
"$y \\in \\{0,1\\}$, verschieden für den Solver",
fontsize=10.5, color=FARBEN["text"], y=1.02)
speichere(figur, "kap_milp_big_m")
if __name__ == "__main__":
faktor = schwelle(M_STRAFF) / schwelle(M_GROSS)
print(f" volle Kapazität ab y = {schwelle(M_STRAFF):.3f} (M = "
f"{M_STRAFF:.0f})")
print(f" volle Kapazität ab y = {schwelle(M_GROSS):.3f} (M = "
f"{M_GROSS:.0f}) → {faktor:.0f}-mal weniger y genügt")
print(f" Trickle Flow im Anhang: y = {TRICKLE_Y:g} gilt als 0, "
f"laesst aber M*y = {TRICKLE_M * TRICKLE_Y:.0f} Einheiten durch.")
zeichne(faktor)