130 lines
5.2 KiB
Python
130 lines
5.2 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
|
|||
|
|
# erzeuge_entartung_polyeder.py
|
|||
|
|
"""
|
|||
|
|
Erzeugt das Entartungs-Diagramm zum LP-Kapitel:
|
|||
|
|
|
|||
|
|
bilder_04/kap_lp_entartung.svg
|
|||
|
|
|
|||
|
|
Links die Geometrie: Drei Geraden laufen durch DIESELBE Ecke. In zwei
|
|||
|
|
Dimensionen legen schon zwei eine Ecke fest - eine ist zu viel, und genau das
|
|||
|
|
ist Entartung. Rechts die Folge davon: Der Schattenpreis jeder Nebenbedingung
|
|||
|
|
ist nicht mehr eine Zahl, sondern eine Spanne.
|
|||
|
|
|
|||
|
|
Warum beides in ein Bild gehoert: Die Geometrie allein erklaert nicht, warum
|
|||
|
|
zwei Solver verschiedene Dualwerte melden duerfen und beide recht haben. Die
|
|||
|
|
Spannen allein sehen nach einem Fehler aus. Nebeneinander ergibt es Sinn.
|
|||
|
|
|
|||
|
|
Modell und Spannen kommen aus Toleranzen_und_Entartung.py - importiert, nicht
|
|||
|
|
abgeschrieben. pruefe_ecke() rechnet zusaetzlich nach, dass wirklich alle drei
|
|||
|
|
Geraden durch den Punkt laufen; waere das nicht so, zeigte das Bild etwas
|
|||
|
|
anderes als der Text daneben behauptet.
|
|||
|
|
|
|||
|
|
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
|||
|
|
python3 bilder_04/erzeuge_entartung_polyeder.py
|
|||
|
|
|
|||
|
|
Benoetigt: numpy, scipy, matplotlib
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
|
|||
|
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
BASIS = os.path.dirname(HIER)
|
|||
|
|
sys.path.insert(0, HIER)
|
|||
|
|
sys.path.insert(0, os.path.join(
|
|||
|
|
BASIS, "Operations_Research_mit_Python_Version_04_Programme"))
|
|||
|
|
|
|||
|
|
from stil_04 import FARBEN, speichere # noqa: E402
|
|||
|
|
import matplotlib.pyplot as plt # noqa: E402
|
|||
|
|
from Toleranzen_und_Entartung import (A_UB, B_UB, NAMEN, # noqa: E402
|
|||
|
|
schattenpreis_spanne)
|
|||
|
|
|
|||
|
|
ECKE = np.array([4 / 3, 4 / 3])
|
|||
|
|
LINIENFARBEN = (FARBEN["haupt"], FARBEN["zweit"], FARBEN["akzent"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def pruefe_ecke() -> None:
|
|||
|
|
"""Alle drei Nebenbedingungen muessen in der Ecke aktiv sein."""
|
|||
|
|
schlupf = B_UB - A_UB @ ECKE
|
|||
|
|
aktiv = np.abs(schlupf) < 1e-9
|
|||
|
|
if not aktiv.all():
|
|||
|
|
raise SystemExit(
|
|||
|
|
f"Entartungsbild: In {tuple(np.round(ECKE, 3))} sind nur "
|
|||
|
|
f"{int(aktiv.sum())} von 3 Bedingungen aktiv (Schlupf "
|
|||
|
|
f"{np.round(schlupf, 6)}). Ohne drei aktive Geraden gibt es keine "
|
|||
|
|
f"Entartung zu zeigen.")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def zeichne(spannen) -> None:
|
|||
|
|
figur, (links, rechts) = plt.subplots(1, 2, figsize=(9.4, 4.3),
|
|||
|
|
gridspec_kw={"width_ratios": [1, 1]})
|
|||
|
|
|
|||
|
|
# --- links: die drei Geraden ---------------------------------------
|
|||
|
|
x = np.linspace(0, 3.2, 200)
|
|||
|
|
for (a, b), grenze, name, farbe in zip(A_UB, B_UB, NAMEN, LINIENFARBEN):
|
|||
|
|
y = (grenze - a * x) / b
|
|||
|
|
links.plot(x, y, "-", color=farbe, linewidth=2.0, label=name, zorder=3)
|
|||
|
|
|
|||
|
|
# Zulaessiger Bereich: Schnitt aller Halbraeume, plus x >= 0.
|
|||
|
|
gitter = np.linspace(0, 3.2, 400)
|
|||
|
|
xx, yy = np.meshgrid(gitter, gitter)
|
|||
|
|
zulaessig = np.ones_like(xx, dtype=bool)
|
|||
|
|
for (a, b), grenze in zip(A_UB, B_UB):
|
|||
|
|
zulaessig &= (a * xx + b * yy <= grenze + 1e-12)
|
|||
|
|
links.contourf(xx, yy, zulaessig.astype(float), levels=[0.5, 1.5],
|
|||
|
|
colors=[FARBEN["gut"]], alpha=0.13, zorder=1)
|
|||
|
|
|
|||
|
|
links.plot(*ECKE, "o", color=FARBEN["fehler"], markersize=9, zorder=5)
|
|||
|
|
links.annotate(r"Optimum $(4/3,\ 4/3)$" "\n" "drei Geraden, eine Ecke",
|
|||
|
|
xy=tuple(ECKE), xytext=(18, 26),
|
|||
|
|
textcoords="offset points", fontsize=9,
|
|||
|
|
color=FARBEN["fehler"], fontweight="bold",
|
|||
|
|
arrowprops=dict(arrowstyle="->", color=FARBEN["fehler"],
|
|||
|
|
linewidth=1.2))
|
|||
|
|
|
|||
|
|
links.set_xlim(0, 3.0)
|
|||
|
|
links.set_ylim(0, 3.0)
|
|||
|
|
links.set_xlabel(r"$x_1$")
|
|||
|
|
links.set_ylabel(r"$x_2$")
|
|||
|
|
links.set_title("Eine Bedingung zu viel", fontsize=10.5,
|
|||
|
|
color=FARBEN["text"])
|
|||
|
|
links.legend(fontsize=8.5, loc="upper right", frameon=False)
|
|||
|
|
|
|||
|
|
# --- rechts: die Spanne je Schattenpreis ---------------------------
|
|||
|
|
y_pos = np.arange(len(NAMEN))[::-1]
|
|||
|
|
for stelle, ((unten, oben), name, farbe) in enumerate(
|
|||
|
|
zip(spannen, NAMEN, LINIENFARBEN)):
|
|||
|
|
y = y_pos[stelle]
|
|||
|
|
rechts.plot([unten, oben], [y, y], "-", color=farbe, linewidth=7,
|
|||
|
|
solid_capstyle="round", alpha=0.75, zorder=3)
|
|||
|
|
rechts.plot([unten, oben], [y, y], "|", color=farbe, markersize=14,
|
|||
|
|
markeredgewidth=2, zorder=4)
|
|||
|
|
rechts.text(oben + 0.012, y, f"{unten:.2f} – {oben:.2f} €",
|
|||
|
|
va="center", fontsize=9, color=FARBEN["text"])
|
|||
|
|
rechts.text(-0.012, y, name, va="center", ha="right", fontsize=9,
|
|||
|
|
color=FARBEN["text"])
|
|||
|
|
|
|||
|
|
rechts.set_yticks([])
|
|||
|
|
rechts.set_ylim(-0.7, len(NAMEN) - 0.3)
|
|||
|
|
rechts.set_xlim(-0.16, max(o for _, o in spannen) * 1.9)
|
|||
|
|
rechts.set_xlabel("Schattenpreis in € je zusätzlicher Stunde")
|
|||
|
|
rechts.set_title("Was der Solver melden darf — jeder Wert der Spanne "
|
|||
|
|
"ist richtig", fontsize=10.5, color=FARBEN["text"])
|
|||
|
|
rechts.grid(axis="y", visible=False)
|
|||
|
|
rechts.spines["left"].set_visible(False)
|
|||
|
|
|
|||
|
|
speichere(figur, "kap_lp_entartung")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
pruefe_ecke()
|
|||
|
|
zielwert = float(np.ones(2) @ ECKE)
|
|||
|
|
spannen = schattenpreis_spanne(zielwert)
|
|||
|
|
for name, (unten, oben) in zip(NAMEN, spannen):
|
|||
|
|
print(f" {name:<18} {unten:>7.4f} bis {oben:>7.4f} EUR")
|
|||
|
|
zeichne(spannen)
|