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>
204 lines
8.6 KiB
Python
204 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# erzeuge_kondition.py
|
|
"""
|
|
Erzeugt das Konditionszahl-Diagramm zum Kapitel Fundament:
|
|
|
|
bilder_04/kap_fundament_kondition.svg statisch, fuer das PDF
|
|
bilder_04/kap_fundament_kondition.png Rasterfassung
|
|
|
|
Zwei Aussagen, beide gerechnet und nicht behauptet:
|
|
|
|
Links - kappa(A) ist keine grobe Faustregel, sondern eine SCHARFE obere
|
|
Schranke. Die gemessene groesste Fehlerverstaerkung liegt ueber
|
|
zwoelf Groessenordnungen hinweg praktisch auf der Geraden y = kappa;
|
|
der Median darunter. Wer also mit kappa = 1e8 rechnet, verliert im
|
|
schlimmsten Fall genau acht Stellen - nicht ungefaehr, sondern
|
|
nachweisbar.
|
|
Rechts - Was die Ruiz-Equilibrierung an einem Modell ausrichtet, in dem
|
|
Euro-Betraege und Tonnen-Angaben in derselben Matrix stehen.
|
|
|
|
Verfahren und Instanz sind wortgleich zu Skalierung_Kondition.py, und
|
|
gerechnet wird hier erneut - so koennen Diagramm und Buchtext nicht
|
|
auseinanderlaufen.
|
|
|
|
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
|
python3 bilder_04/erzeuge_kondition.py
|
|
|
|
Benoetigt: numpy, matplotlib
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
|
|
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
|
|
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
|
|
|
|
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
BILDER = os.path.join(BASIS, "bilder_04")
|
|
|
|
INDIGO, CYAN, AMBER, GRUEN = "#4338ca", "#0891b2", "#b45309", "#059669"
|
|
|
|
STOERUNG = 1e-10 # relative Stoerung der rechten Seite
|
|
|
|
GUT = np.array([[2.0, 1.0], [1.0, 3.0]])
|
|
SCHLECHT = np.array([[1.0, 1.0], [1.0, 1.0 + 1e-8]])
|
|
|
|
|
|
# --- Messung: wortgleich zu Skalierung_Kondition.py -------------------------
|
|
|
|
def fehlerverstaerkung(A: np.ndarray, rng: np.random.Generator,
|
|
versuche: int = 200) -> tuple[float, float]:
|
|
"""Stoert b relativ um STOERUNG und misst den Faktor, um den der
|
|
Loesungsfehler groesser ausfaellt. Liefert (Median, Maximum)."""
|
|
x_wahr = np.ones(A.shape[1])
|
|
b = A @ x_wahr
|
|
faktoren = []
|
|
for _ in range(versuche):
|
|
richtung = rng.normal(size=b.size)
|
|
richtung /= np.linalg.norm(richtung)
|
|
b_gestoert = b + STOERUNG * np.linalg.norm(b) * richtung
|
|
x_gestoert = np.linalg.solve(A, b_gestoert)
|
|
rel_x = np.linalg.norm(x_gestoert - x_wahr) / np.linalg.norm(x_wahr)
|
|
faktoren.append(rel_x / STOERUNG)
|
|
return float(np.median(faktoren)), float(np.max(faktoren))
|
|
|
|
|
|
def ruiz_equilibrierung(A: np.ndarray, durchlaeufe: int = 20) -> np.ndarray:
|
|
"""Zeilen- und Spaltenskalierung, bis alle Maxima nahe 1 liegen."""
|
|
A_s = A.astype(float).copy()
|
|
for _ in range(durchlaeufe):
|
|
zeilen_max = np.abs(A_s).max(axis=1)
|
|
zeilen_max[zeilen_max == 0] = 1.0
|
|
A_s = (1.0 / np.sqrt(zeilen_max))[:, None] * A_s
|
|
spalten_max = np.abs(A_s).max(axis=0)
|
|
spalten_max[spalten_max == 0] = 1.0
|
|
A_s = A_s * (1.0 / np.sqrt(spalten_max))[None, :]
|
|
return A_s
|
|
|
|
|
|
def mischmodell() -> np.ndarray:
|
|
"""Das Modell mit den vier unvertraeglichen Einheiten.
|
|
|
|
Skalierung_Kondition.py zieht diese Matrix aus DEMSELBEN Generator, aus dem
|
|
zuvor schon die Stoerungen von Experiment 1 kamen. Damit hier bitgleich
|
|
dieselbe Matrix entsteht - und das Diagramm dieselbe Konditionszahl zeigt
|
|
wie der abgedruckte Programmlauf -, wird die Ziehungsreihenfolge exakt
|
|
nachgespielt: erst 2 x 200 Stoerungen, dann die Grundmatrix.
|
|
"""
|
|
rng = np.random.default_rng(42)
|
|
for A in (GUT, SCHLECHT):
|
|
fehlerverstaerkung(A, rng)
|
|
grundmatrix = rng.uniform(0.5, 2.0, size=(4, 4))
|
|
einheiten = np.array([1e7, 1e-3, 1e5, 1e-2])
|
|
return grundmatrix * einheiten[:, None]
|
|
|
|
|
|
def messreihe() -> tuple[list[float], list[float], list[float]]:
|
|
"""Eine Familie fast paralleler Zeilen: A = [[1, 1], [1, 1 + eps]].
|
|
|
|
Mit fallendem eps waechst kappa(A) um zwoelf Groessenordnungen, ohne dass
|
|
sich sonst irgendetwas am Modell aendert. Genau das macht die Kurve
|
|
aussagekraeftig: Der einzige Unterschied ist die Kondition.
|
|
"""
|
|
# Eigener Generator, damit die Messreihe den nachgespielten Strom in
|
|
# mischmodell() nicht stoert.
|
|
rng = np.random.default_rng(7)
|
|
kappas, mediane, maxima = [], [], []
|
|
for exponent in np.arange(-1.0, -12.5, -0.5):
|
|
eps = 10.0 ** exponent
|
|
A = np.array([[1.0, 1.0], [1.0, 1.0 + eps]])
|
|
median, maximum = fehlerverstaerkung(A, rng)
|
|
kappas.append(float(np.linalg.cond(A)))
|
|
mediane.append(median)
|
|
maxima.append(maximum)
|
|
return kappas, mediane, maxima
|
|
|
|
|
|
# --- Zeichnen ---------------------------------------------------------------
|
|
|
|
def zeichne(kappas, mediane, maxima, kappa_vorher, kappa_nachher) -> None:
|
|
figur, (links, rechts) = plt.subplots(
|
|
1, 2, figsize=(10.4, 4.3), gridspec_kw={"width_ratios": [1.75, 1]})
|
|
|
|
# --- links: Verstaerkung gegen kappa ---
|
|
# Die Gerade reicht bewusst ueber die Messpunkte hinaus: Innerhalb des
|
|
# Messbereichs liegt sie exakt unter der Messkurve - und genau das ist die
|
|
# Aussage. Sichtbar bleibt sie dadurch an den Enden.
|
|
grenze = np.array([min(kappas) / 30, max(kappas) * 30])
|
|
links.plot(grenze, grenze, color="#94a3b8", linewidth=3.0, linestyle="-",
|
|
alpha=0.55, zorder=1,
|
|
label="theoretische Schranke $y=\\kappa(A)$")
|
|
links.plot(kappas, maxima, "o-", color=INDIGO, markersize=4.5, linewidth=1.4,
|
|
zorder=3, label="gemessen: größte Verstärkung")
|
|
links.plot(kappas, mediane, "s-", color=CYAN, markersize=4.0, linewidth=1.4,
|
|
zorder=2, label="gemessen: Median")
|
|
|
|
marken_rng = np.random.default_rng(7)
|
|
for A, name, versatz in ((GUT, "gut konditioniert", (14, -4)),
|
|
(SCHLECHT, "fast parallele Zeilen", (-12, 14))):
|
|
_, maximum = fehlerverstaerkung(A, marken_rng)
|
|
k = float(np.linalg.cond(A))
|
|
links.scatter([k], [maximum], s=95, facecolors="none", edgecolors=AMBER,
|
|
linewidths=2.0, zorder=4)
|
|
links.annotate(name, (k, maximum), textcoords="offset points",
|
|
xytext=versatz, fontsize=8, color=AMBER,
|
|
ha="right" if versatz[0] < 0 else "left")
|
|
|
|
links.set_xscale("log")
|
|
links.set_yscale("log")
|
|
links.set_xlabel("Konditionszahl $\\kappa(A)$")
|
|
links.set_ylabel("Fehlerverstärkung (Faktor)")
|
|
links.set_title("$\\kappa(A)$ ist eine scharfe Schranke, keine Faustregel",
|
|
fontsize=10.5)
|
|
links.grid(which="both", linestyle=":", alpha=0.45)
|
|
links.legend(fontsize=8, loc="upper left", frameon=False)
|
|
links.annotate("Die Messung liegt auf der Schranke:\n"
|
|
"$\\kappa$ wird im schlimmsten Fall\nvollständig ausgeschöpft.",
|
|
xy=(0.97, 0.06), xycoords="axes fraction", fontsize=8,
|
|
ha="right", va="bottom", color="#475569")
|
|
for rand in ("top", "right"):
|
|
links.spines[rand].set_visible(False)
|
|
|
|
# --- rechts: was Ruiz ausrichtet ---
|
|
balken = rechts.bar(["vorher", "nach Ruiz"], [kappa_vorher, kappa_nachher],
|
|
color=[AMBER, GRUEN], width=0.55)
|
|
rechts.set_yscale("log")
|
|
rechts.set_ylim(1, kappa_vorher * 40)
|
|
for stab, wert in zip(balken, (kappa_vorher, kappa_nachher)):
|
|
rechts.annotate(f"{wert:.2e}", (stab.get_x() + stab.get_width() / 2, wert),
|
|
textcoords="offset points", xytext=(0, 6),
|
|
ha="center", fontsize=9, fontweight="bold")
|
|
stellen = int(round(np.log10(kappa_vorher / kappa_nachher)))
|
|
rechts.set_title(f"Ruiz-Equilibrierung:\n{stellen} Größenordnungen besser",
|
|
fontsize=10.5)
|
|
rechts.set_ylabel("$\\kappa(A)$")
|
|
rechts.grid(axis="y", which="both", linestyle=":", alpha=0.45)
|
|
for rand in ("top", "right"):
|
|
rechts.spines[rand].set_visible(False)
|
|
|
|
figur.tight_layout()
|
|
os.makedirs(BILDER, exist_ok=True)
|
|
for endung in ("svg", "png"):
|
|
pfad = os.path.join(BILDER, f"kap_fundament_kondition.{endung}")
|
|
figur.savefig(pfad, format=endung, dpi=160,
|
|
metadata={"Date": None} if endung == "svg" else None)
|
|
print(f"geschrieben: {pfad}")
|
|
plt.close(figur)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
kappas, mediane, maxima = messreihe()
|
|
modell = mischmodell()
|
|
kappa_vorher = float(np.linalg.cond(modell))
|
|
kappa_nachher = float(np.linalg.cond(ruiz_equilibrierung(modell)))
|
|
print(f"Messreihe: {len(kappas)} Matrizen, kappa von {min(kappas):.2e} "
|
|
f"bis {max(kappas):.2e}")
|
|
print(f"Ruiz: kappa {kappa_vorher:.2e} -> {kappa_nachher:.2e}")
|
|
zeichne(kappas, mediane, maxima, kappa_vorher, kappa_nachher)
|