Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
b7af2f1d9a
468 changed files with 262141 additions and 0 deletions
180
OR_HTML_04/programme/Betriebsueberwachung.py
Normal file
180
OR_HTML_04/programme/Betriebsueberwachung.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
#!/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)
|
||||
Loading…
Reference in a new issue