180 lines
7 KiB
Python
180 lines
7 KiB
Python
|
|
#!/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)
|