#!/usr/bin/env python3 # Betriebsueberwachung.py """ Kapitel Praxisfallen: Das Modell, das still aufgehoert hat zu optimieren. Ein Optimierungsmodell geht in Betrieb und laeuft jede Nacht. Es liefert zuverlaessig eine Zahl, die Job-Dauer bleibt konstant, es gibt keine Fehlermeldung. Alles sieht gut aus. Waehrenddessen waechst das Unternehmen. Aus 15 Lagern werden 40, dann 80. Das Zeitlimit von drei Sekunden - vor zwei Jahren grosszuegig bemessen - reicht irgendwann nicht mehr. Der Solver bricht ab und liefert die beste Loesung, die er bis dahin gefunden hat. Das ist nicht falsch. Es ist sogar richtig so. Aber es ist etwas anderes als das, was das Modell einmal geleistet hat - und niemand merkt es, weil im Protokoll nur die Kosten stehen. Dieses Programm zeigt die Verschlechterung und die Ueberwachung, die sie sichtbar macht. WICHTIG: ortools wird hier nicht importiert (Konflikt mit highspy, siehe Kapitel Oekosystem). Benoetigt: numpy, highspy """ from __future__ import annotations import time from dataclasses import dataclass import numpy as np import highspy ZEITLIMIT = 3.0 # das Budget, das der Nachtjob hat GAP_WARNSCHWELLE = 0.02 # ab 2 % Gap wollen wir es wissen ZEITAUSLASTUNG_WARNSCHWELLE = 0.9 @dataclass class Laufprotokoll: """Was nach JEDEM Produktivlauf protokolliert gehoert. Die meisten Systeme schreiben nur 'kosten'. Genau deshalb faellt eine schleichende Verschlechterung ueber Monate nicht auf. """ zeitpunkt: str lager: int kunden: int status: str kosten: float gap: float laufzeit: float knoten: int @property def warnungen(self) -> list[str]: meldungen = [] if self.status != "Optimal": meldungen.append(f"nicht beweisbar optimal ({self.status})") if self.gap > GAP_WARNSCHWELLE: meldungen.append(f"Gap {self.gap:.1%} ueber Schwelle " f"{GAP_WARNSCHWELLE:.0%}") if self.laufzeit > ZEITAUSLASTUNG_WARNSCHWELLE * ZEITLIMIT: meldungen.append(f"Zeitbudget zu {self.laufzeit / ZEITLIMIT:.0%} " f"ausgeschoepft") return meldungen def plane_netzwerk(n_lager: int, n_kunden: int, zeitlimit: float, saat: int = 7) -> tuple[str, float, float, float, int]: """Standortplanung wie in Kapitel MILP, nur mit wachsender Groesse.""" rng = np.random.default_rng(saat) fixkosten = rng.uniform(3000, 9000, n_lager) transport = rng.uniform(5, 60, (n_lager, n_kunden)) bedarf = rng.uniform(10, 60, n_kunden) kapazitaet = np.full(n_lager, bedarf.sum() * 0.22) modell = highspy.Highs() modell.setOptionValue("output_flag", False) modell.setOptionValue("time_limit", zeitlimit) anzahl_x = n_lager * n_kunden unendlich = highspy.kHighsInf modell.addVars(anzahl_x, np.zeros(anzahl_x), np.full(anzahl_x, unendlich)) modell.addVars(n_lager, np.zeros(n_lager), np.ones(n_lager)) for i in range(n_lager): modell.changeColIntegrality(anzahl_x + i, highspy.HighsVarType.kInteger) modell.changeColCost(anzahl_x + i, fixkosten[i]) for j in range(n_kunden): modell.changeColCost(i * n_kunden + j, transport[i, j]) for j in range(n_kunden): index = np.array([i * n_kunden + j for i in range(n_lager)], dtype=np.int32) modell.addRow(bedarf[j], bedarf[j], len(index), index, np.ones(len(index))) for i in range(n_lager): index = np.array([i * n_kunden + j for j in range(n_kunden)] + [anzahl_x + i], dtype=np.int32) werte = np.concatenate([np.ones(n_kunden), [-kapazitaet[i]]]) modell.addRow(-unendlich, 0.0, len(index), index, werte) t0 = time.perf_counter() modell.run() laufzeit = time.perf_counter() - t0 info = modell.getInfo() return (modell.modelStatusToString(modell.getModelStatus()), info.objective_function_value, info.mip_gap, laufzeit, info.mip_node_count) if __name__ == "__main__": # So ist das Unternehmen ueber zwei Jahre gewachsen. entwicklung = [("2024-Q1", 15, 40), ("2024-Q3", 25, 70), ("2025-Q1", 40, 110), ("2025-Q3", 60, 160), ("2026-Q1", 80, 220)] print("=" * 84) print(" WAS DAS PROTOKOLL ZEIGT - UND WAS ES ZEIGEN SOLLTE") print("=" * 84) print(f"Derselbe Nachtjob, unveraendert, mit {ZEITLIMIT:.0f} Sekunden Zeitlimit.\n") protokolle = [] for zeitpunkt, lager, kunden in entwicklung: status, kosten, gap, laufzeit, knoten = plane_netzwerk( lager, kunden, ZEITLIMIT) protokolle.append(Laufprotokoll(zeitpunkt, lager, kunden, status, kosten, gap, laufzeit, knoten)) print("So sieht das ueblich gefuehrte Protokoll aus:\n") print(f" {'Zeitpunkt':<10} {'Kosten':>12}") print(" " + "-" * 24) for p in protokolle: print(f" {p.zeitpunkt:<10} {p.kosten:>12,.0f}") print("\n -> Die Kosten steigen. Das Unternehmen waechst ja auch.") print(" Nichts an dieser Tabelle deutet auf ein Problem hin.") print("\n" + "-" * 84) print("So sieht ein vollstaendiges Protokoll aus:\n") print(f" {'Zeitpunkt':<10} {'Groesse':>10} {'Status':>20} {'Kosten':>11} " f"{'Gap':>8} {'Zeit':>7}") print(" " + "-" * 80) for p in protokolle: groesse = f"{p.lager}x{p.kunden}" print(f" {p.zeitpunkt:<10} {groesse:>10} {p.status:>20} " f"{p.kosten:>11,.0f} {p.gap * 100:7.2f} % {p.laufzeit:6.2f}s") print("\n" + "-" * 84) print("Und so sehen die Warnungen aus, die daraus folgen:\n") for p in protokolle: if p.warnungen: print(f" {p.zeitpunkt}: " + "; ".join(p.warnungen)) else: print(f" {p.zeitpunkt}: in Ordnung") letzte = protokolle[-1] erste_warnung = next(p for p in protokolle if p.warnungen) print("\n" + "=" * 84) print(" WAS DA PASSIERT IST") print("=" * 84) print(f"Seit {erste_warnung.zeitpunkt} erreicht der Job das Zeitlimit und liefert") print("nicht mehr das Optimum, sondern die beste bis dahin gefundene Loesung.") print(f"Im letzten Lauf betraegt der Abstand zum Bestmoeglichen " f"{letzte.gap:.1%}.") print() print("Der Job ist nicht abgestuerzt. Er hat keine Fehlermeldung erzeugt.") print("Die Laufzeit ist sogar bemerkenswert STABIL geblieben - genau deshalb,") print("weil das Zeitlimit greift. Ein Ueberwachungssystem, das auf Abstuerze") print("und Laufzeitspitzen achtet, sieht hier nichts.") print() print("Drei Zahlen gehoeren deshalb in jedes Protokoll eines Optimierungsjobs:") print(" * der STATUS - 'Optimal' oder etwas anderes?") print(" * der GAP - wie weit ist die Loesung vom Bestmoeglichen entfernt?") print(" * die LAUFZEIT im Verhaeltnis zum Limit - wie nah am Anschlag?") print() print("Die dritte ist die frueheste Warnung: Sie steigt, lange bevor der Gap") print("sichtbar wird, und gibt Zeit zum Handeln.") print("=" * 84)