#!/usr/bin/env python3 # Sensitivitaetsanalyse.py """ Kapitel LP: Schattenpreis- und Sensitivitätsanalyse mit SciPy und HiGHS. Achtung: Ohne Vorzeichenumkehr der Dualwerte waere die Handlungsempfehlung strukturell immer "kein Zukauf noetig" - selbst bei harten Engpaessen. Dieses Programm zeigt die korrekte Vorzeichenbehandlung. """ import numpy as np from scipy.optimize import linprog # --- Modell: Maximiere Deckungsbeitrag aus 3 Produkten --------------------- # max 40*x1 + 30*x2 + 50*x3 DECKUNGSBEITRAG = np.array([40.0, 30.0, 50.0]) # Ressourcenverbrauch je Produkt (Zeile = Ressource, Spalte = Produkt) VERBRAUCH = np.array([ [2.0, 1.0, 3.0], # Montagezeit [1.0, 2.0, 1.0], # Lackierzeit [1.0, 0.5, 2.0], # Qualitätsprüfung ]) KAPAZITAET = np.array([120.0, 80.0, 50.0]) # Stunden RESSOURCEN = ["Montage", "Lackieren", "Qualitätsprüfung"] PRODUKTE = ["Produkt 1", "Produkt 2", "Produkt 3"] ANGEBOTSPREIS_PRUEFSTUNDE = 18.0 # EUR/h - lohnt sich der Zukauf? def analysiere(): # linprog MINIMIERT -> Zielfunktion negieren ergebnis = linprog(c=-DECKUNGSBEITRAG, A_ub=VERBRAUCH, b_ub=KAPAZITAET, bounds=[(0, None)] * len(DECKUNGSBEITRAG), method="highs") if not ergebnis.success: raise SystemExit(f"Kein Optimum gefunden: {ergebnis.message}") max_gewinn = -ergebnis.fun mengen = ergebnis.x schlupf = ergebnis.slack # ------------------------------------------------------------------ # DER ENTSCHEIDENDE PUNKT: # Weil wir zur Maximierung negiert haben, sind die Dualwerte aus # linprog fuer "<="-Bedingungen <= 0. Zurueckdrehen! # ------------------------------------------------------------------ schattenpreise = -ergebnis.ineqlin.marginals print("=" * 74) print(" PRIMALE UND DUALE ERGEBNISANALYSE (SENSITIVITAET)") print("=" * 74) print(f"Maximaler Deckungsbeitrag: {max_gewinn:,.2f} EUR\n") print("--- Primalloesung: optimale Produktionsmengen ---") for name, menge in zip(PRODUKTE, mengen): print(f" * {name}: {menge:8.2f} Stueck") print("\n--- Duale Analyse: Schattenpreise und Auslastung ---") for i, name in enumerate(RESSOURCEN): kapazitaet = KAPAZITAET[i] genutzt = kapazitaet - schlupf[i] auslastung = genutzt / kapazitaet * 100 preis = schattenpreise[i] bindend = abs(schlupf[i]) < 1e-9 print(f"\nRessource '{name}':") print(f" Auslastung: {genutzt:6.1f} / {kapazitaet:6.1f} h ({auslastung:5.1f} %)" f" -> {'ENGPASS' if bindend else 'Reserve: %.1f h' % schlupf[i]}") print(f" Schattenpreis: {preis:6.2f} EUR je zusaetzlicher Stunde") if preis > 1e-9: print(f" >> Zusaetzliche Stunden lohnen sich bis zu einem Preis von " f"{preis:.2f} EUR/h.") else: print(f" >> Kein Zukauf noetig - die Kapazitaet ist nicht erschoepft.") # --- Konkrete Kaufentscheidung --------------------------------------- preis_pruefung = schattenpreise[RESSOURCEN.index("Qualitätsprüfung")] marge = preis_pruefung - ANGEBOTSPREIS_PRUEFSTUNDE print("\n" + "-" * 74) print(f"ENTSCHEIDUNG: Pruefstunden werden fuer " f"{ANGEBOTSPREIS_PRUEFSTUNDE:.2f} EUR/h angeboten.") print(f" Schattenpreis: {preis_pruefung:6.2f} EUR/h") print(f" Angebotspreis: {ANGEBOTSPREIS_PRUEFSTUNDE:6.2f} EUR/h") print(f" Marge: {marge:+6.2f} EUR je zugekaufter Stunde") print(f" >> {'ZUKAUFEN' if marge > 0 else 'NICHT ZUKAUFEN'}") # --- Numerische Gegenprobe: Kapazitaet wirklich um 1 erhoehen -------- kapazitaet_plus = KAPAZITAET.copy() kapazitaet_plus[RESSOURCEN.index("Qualitätsprüfung")] += 1.0 gegenprobe = linprog(c=-DECKUNGSBEITRAG, A_ub=VERBRAUCH, b_ub=kapazitaet_plus, bounds=[(0, None)] * 3, method="highs") tatsaechlicher_zuwachs = -gegenprobe.fun - max_gewinn print("\n--- Gegenprobe: Modell mit +1 Pruefstunde neu geloest ---") print(f" Vorhergesagt (Schattenpreis): {preis_pruefung:8.4f} EUR") print(f" Tatsaechlich gemessen: {tatsaechlicher_zuwachs:8.4f} EUR") assert abs(tatsaechlicher_zuwachs - preis_pruefung) < 1e-6, \ "Schattenpreis stimmt nicht mit der Messung ueberein!" print(" -> Der Schattenpreis ist bestaetigt.") # --- Komplementaerer Schlupf pruefen -------------------------------- for s, y in zip(schlupf, schattenpreise): assert abs(s * y) < 1e-6, "Komplementaerer Schlupf verletzt!" print("\nPruefung: komplementaerer Schlupf fuer alle Ressourcen erfuellt.") print("=" * 74) if __name__ == "__main__": analysiere()