operations_research/bilder_04/erzeuge_kombinatorik_wand.py

149 lines
5.4 KiB
Python
Raw 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_kombinatorik_wand.py
"""
Erzeugt das Wachstumsdiagramm zur Einfuehrung:
bilder_04/kap_einfuehrung_kombinatorik.svg
Die Handrechnung des Abschnitts rechnet vor, wie lange vollstaendiges
Durchprobieren dauert, wenn ein Rechner eine Milliarde Reihenfolgen je Sekunde
prueft. Das Bild macht daraus eine Kurve - und zeigt daneben, was polynomiales
Wachstum im selben Massstab bedeutet: eine Linie, die am unteren Rand klebt.
Der didaktische Punkt ist nicht "n! waechst schnell", sondern die SCHMALHEIT
des Uebergangs: Zwischen n=15 (22 Minuten, machbar) und n=20 (77 Jahre,
hoffnungslos) liegen fuenf Auftraege. Deshalb der markierte Streifen.
ZU DEN ZAHLEN: Sie werden gerechnet, nicht eingetragen. pruefe_gegen_buch()
vergleicht sie anschliessend mit der abgedruckten Tabelle des Kapitels - weicht
eine ab, bricht das Skript ab, statt ein Bild zu erzeugen, das der Tabelle
daneben widerspricht.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_kombinatorik_wand.py
Benoetigt: numpy, matplotlib
"""
from __future__ import annotations
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from stil_04 import FARBEN, neue_figur, speichere # noqa: E402
PRUEFUNGEN_JE_SEKUNDE = 1e9
SEKUNDEN_JE_JAHR = 365.25 * 24 * 3600
# Die vier Marken der Handrechnung. Werte NUR zum Gegenpruefen - gezeichnet
# wird, was gerechnet wurde.
TABELLE_IM_BUCH = {
10: "0,004 Sekunden",
15: "22 Minuten",
20: "77 Jahre",
25: "490 Millionen Jahre",
}
def sekunden(n: int) -> float:
return math.factorial(n) / PRUEFUNGEN_JE_SEKUNDE
def _signifikant(wert: float, stellen: int = 2) -> int:
"""Rundet auf 'stellen' signifikante Ziffern.
Die Tabelle des Kapitels tut genau das: 491,5 Millionen Jahre stehen dort
als '490 Millionen'. Volle Millionen zu runden ergaebe 492 - und damit ein
Bild, das der Tabelle daneben widerspricht."""
if wert == 0:
return 0
ordnung = math.floor(math.log10(abs(wert)))
faktor = 10 ** (ordnung - stellen + 1)
return int(round(wert / faktor) * faktor)
def lesbar(n: int) -> str:
"""Formatiert eine Dauer so, wie die Tabelle des Kapitels es tut."""
s = sekunden(n)
if s < 1:
return f"{s:.3f}".replace(".", ",") + " Sekunden"
if s < 3600:
return f"{round(s / 60)} Minuten"
jahre = s / SEKUNDEN_JE_JAHR
if jahre < 1000:
return f"{round(jahre)} Jahre"
if jahre < 1e9:
return f"{_signifikant(jahre / 1e6)} Millionen Jahre"
return f"{jahre:.1e} Jahre"
def pruefe_gegen_buch() -> None:
"""Die Zahlen im Bild muessen die der abgedruckten Tabelle sein."""
for n, erwartet in TABELLE_IM_BUCH.items():
ist = lesbar(n)
if ist != erwartet:
raise SystemExit(
f"Kombinatorik-Wand: Fuer n={n} ergibt die Rechnung {ist!r}, "
f"die Tabelle im Kapitel nennt {erwartet!r}. Bild und Text "
f"wuerden sich widersprechen.")
def zeichne() -> None:
figur, achse = neue_figur(breite=7.8, hoehe=5.0)
n = np.arange(1, 31)
fakultaet = np.array([sekunden(int(k)) for k in n])
quadrat = n.astype(float) ** 2 / PRUEFUNGEN_JE_SEKUNDE
kubik = n.astype(float) ** 3 / PRUEFUNGEN_JE_SEKUNDE
achse.semilogy(n, fakultaet, "-", color=FARBEN["fehler"], linewidth=2.4,
label=r"$n!$ — alle Reihenfolgen durchprobieren", zorder=4)
achse.semilogy(n, kubik, "-", color=FARBEN["zweit"], linewidth=1.8,
label=r"$n^3$", zorder=3)
achse.semilogy(n, quadrat, "-", color=FARBEN["haupt"], linewidth=1.8,
label=r"$n^2$", zorder=3)
# Der schmale Uebergang, um den es geht.
achse.axvspan(15, 20, color=FARBEN["warnung"], alpha=0.12, zorder=1)
achse.annotate("fünf Aufträge mehr —\n22 Minuten wird zu 77 Jahren",
xy=(17.5, sekunden(17) * 0.02), fontsize=9,
color=FARBEN["warnung"], ha="center", fontweight="bold")
# Waagerechte Bezugsmarken: was ein Mensch noch abwartet.
for hoehe, text in ((1.0, "1 Sekunde"), (3600.0, "1 Stunde"),
(SEKUNDEN_JE_JAHR, "1 Jahr")):
achse.axhline(hoehe, color=FARBEN["linie"], linewidth=0.9,
linestyle="--", zorder=2)
achse.text(30.4, hoehe, text, fontsize=8, color=FARBEN["gedaempft"],
va="center")
for k in sorted(TABELLE_IM_BUCH):
achse.plot([k], [sekunden(k)], "o", color=FARBEN["fehler"],
markersize=6, zorder=5)
achse.annotate(f"$n={k}$: {lesbar(k)}",
xy=(k, sekunden(k)), xytext=(-6, 12),
textcoords="offset points", fontsize=8.5,
color=FARBEN["text"], ha="right",
arrowprops=dict(arrowstyle="-", color=FARBEN["linie"],
linewidth=0.8))
achse.set_xlabel("Zahl der Aufträge $n$")
achse.set_ylabel(r"Rechenzeit in Sekunden bei $10^9$ Prüfungen/s")
achse.set_title("Warum Ausprobieren scheitert — dieselbe Achse für alle "
"drei Kurven", fontsize=10.5, color=FARBEN["text"])
achse.set_xlim(1, 30)
achse.set_ylim(1e-9, 1e26)
achse.legend(fontsize=9, loc="upper left", frameon=False)
speichere(figur, "kap_einfuehrung_kombinatorik")
if __name__ == "__main__":
pruefe_gegen_buch()
print("Zahlen stimmen mit der Tabelle des Kapitels überein.")
zeichne()