#!/usr/bin/env python3 # Visualisierung_Loesungsraum.py """ Kapitel Fundament: Geometrische Visualisierung eines 2D-Optimierungsraums. Ecken werden berechnet, auf Zulässigkeit geprüft, bewertet und eingezeichnet. Zusätzlich wird das ganzzahlige Optimum systematisch bestimmt statt behauptet. """ import itertools import os import numpy as np import matplotlib matplotlib.use("Agg") # kein Bildschirm nötig import matplotlib.pyplot as plt OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output") os.makedirs(OUTPUT_DIR, exist_ok=True) # --- Modell (identisch zum Kapitel Einfuehrung) ---------------------------- # max 150*xA + 250*xB # u.d.N. 2*xA + 5*xB <= 40 (vCPU) # 4*xA + 6*xB <= 60 (RAM) # 1*xA + 0*xB <= 8 (Marktliquidität) c = np.array([150.0, 250.0]) A = np.array([[2.0, 5.0], [4.0, 6.0], [1.0, 0.0]]) b = np.array([40.0, 60.0, 8.0]) restriktionsnamen = ["vCPU", "RAM", "Marktlimit"] def ist_zulaessig(punkt, tol=1e-7): return np.all(A @ punkt <= b + tol) and np.all(punkt >= -tol) def berechne_ecken(): """ Ecken = Schnittpunkte je zweier Begrenzungsgeraden, die zulässig sind. Begrenzungen sind die 3 Restriktionen plus die beiden Achsen xA=0, xB=0. """ geraden = [(A[i], b[i]) for i in range(len(b))] geraden.append((np.array([1.0, 0.0]), 0.0)) # xA = 0 geraden.append((np.array([0.0, 1.0]), 0.0)) # xB = 0 ecken = [] for (n1, d1), (n2, d2) in itertools.combinations(geraden, 2): M = np.array([n1, n2]) if abs(np.linalg.det(M)) < 1e-9: # parallel -> kein Schnittpunkt continue p = np.linalg.solve(M, np.array([d1, d2])) if ist_zulaessig(p) and not any(np.allclose(p, e) for e in ecken): ecken.append(p) return np.array(ecken) def bestes_ganzzahliges(): """Vollständige Suche über das kleine Gitter - hier zulässig, weil winzig.""" bester_wert, bester_punkt = -np.inf, None for xa in range(0, 21): for xb in range(0, 21): p = np.array([float(xa), float(xb)]) if ist_zulaessig(p) and c @ p > bester_wert: bester_wert, bester_punkt = c @ p, p return bester_punkt, bester_wert # --- Analyse --------------------------------------------------------------- ecken = berechne_ecken() werte = ecken @ c reihenfolge = np.argsort(-werte) print("=" * 62) print(" ECKEN DES ZULÄSSIGEN POLYEDERS (nach Zielwert sortiert)") print("=" * 62) print(f"{'x_A':>8} {'x_B':>8} {'Z = 150 xA + 250 xB':>24}") print("-" * 62) for i in reihenfolge: print(f"{ecken[i, 0]:>8.2f} {ecken[i, 1]:>8.2f} {werte[i]:>24,.2f} EUR") lp_punkt, lp_wert = ecken[reihenfolge[0]], werte[reihenfolge[0]] ip_punkt, ip_wert = bestes_ganzzahliges() print("-" * 62) print(f"Kontinuierliches Optimum (LP): x = ({lp_punkt[0]:.2f}, {lp_punkt[1]:.2f}), " f"Z = {lp_wert:,.2f} EUR") print(f"Ganzzahliges Optimum (IP): x = ({ip_punkt[0]:.0f}, {ip_punkt[1]:.0f}), " f"Z = {ip_wert:,.2f} EUR") print(f"Preis der Ganzzahligkeit: {lp_wert - ip_wert:,.2f} EUR " f"({(1 - ip_wert / lp_wert) * 100:.2f} %)") print("=" * 62) # --- Zeichnung ------------------------------------------------------------- gitter = np.linspace(0, 15, 400) xa_gitter, xb_gitter = np.meshgrid(gitter, gitter) plt.figure(figsize=(10, 8)) # Restriktionsgeraden plt.plot(gitter, (40 - 2 * gitter) / 5, color="tab:blue", lw=2, label=r"$2x_A + 5x_B \leq 40$ (vCPU)") plt.plot(gitter, (60 - 4 * gitter) / 6, color="tab:green", lw=2, label=r"$4x_A + 6x_B \leq 60$ (RAM)") plt.axvline(x=8, color="tab:orange", lw=2, label=r"$x_A \leq 8$ (Marktlimit)") # Zulässiger Bereich maske = ((2 * xa_gitter + 5 * xb_gitter <= 40) & (4 * xa_gitter + 6 * xb_gitter <= 60) & (xa_gitter <= 8) & (xa_gitter >= 0) & (xb_gitter >= 0)) plt.imshow(maske.astype(int), extent=(0, 15, 0, 15), origin="lower", cmap="Greys", alpha=0.25, aspect="auto") # Höhenlinien der Zielfunktion Z = 150 * xa_gitter + 250 * xb_gitter hoehen = plt.contour(xa_gitter, xb_gitter, Z, levels=[500, 1000, 1500, 2000, 2375], colors="purple", linestyles="--", alpha=0.7) plt.clabel(hoehen, inline=True, fontsize=9, fmt="Z = %1.0f EUR") # Ecken einzeichnen - jetzt werden sie tatsächlich benutzt plt.scatter(ecken[:, 0], ecken[:, 1], s=70, facecolors="white", edgecolors="black", zorder=4, label="Ecken des Polyeders") for e, w in zip(ecken, werte): plt.annotate(f"({e[0]:.1f}, {e[1]:.1f})\nZ={w:,.0f}", (e[0], e[1]), textcoords="offset points", xytext=(6, 6), fontsize=8) plt.scatter([lp_punkt[0]], [lp_punkt[1]], color="purple", marker="D", s=110, zorder=5, label=f"LP-Optimum ({lp_punkt[0]:.1f}, {lp_punkt[1]:.1f})") plt.scatter([ip_punkt[0]], [ip_punkt[1]], color="red", s=170, zorder=6, label=f"Ganzzahliges Optimum ({ip_punkt[0]:.0f}, {ip_punkt[1]:.0f})") plt.xlim(0, 12) plt.ylim(0, 10) plt.xlabel("Anzahl Arbitrage-Bots ($x_A$)", fontsize=11) plt.ylabel("Anzahl Trendfolge-Bots ($x_B$)", fontsize=11) plt.title("Polyeder des zulässigen Bereichs mit Niveaulinien der Zielfunktion", fontsize=13) plt.grid(True, linestyle=":", alpha=0.6) plt.legend(loc="upper right", framealpha=0.9) plt.tight_layout() ziel = os.path.join(OUTPUT_DIR, "feasible_region_2d.png") plt.savefig(ziel, dpi=150) print(f"Visualisierung gespeichert unter '{ziel}'")