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
170
OR_HTML_04/programme/Modellierungsschichten.py
Normal file
170
OR_HTML_04/programme/Modellierungsschichten.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Modellierungsschichten.py
|
||||
"""
|
||||
Kapitel Oekosystem: Pyomo und Linopy - zwei Modellierungsschichten fuer grosse Modelle.
|
||||
|
||||
Geloest wird dasselbe Produktionsproblem wie im Vierfach-Vergleich:
|
||||
|
||||
max 10*x1 + 15*x2 + 25*x3
|
||||
u.d.N. x1 + x2 + 2*x3 <= 40
|
||||
2*x1 + 3*x2 + x3 <= 50
|
||||
x >= 0
|
||||
|
||||
Handrechnung: Z* = 530 bei x = (0, 12, 14).
|
||||
|
||||
Der Vergleich zeigt die beiden Denkweisen:
|
||||
* Pyomo - algebraisch, indexbasiert, Industriestandard fuer Grossmodelle,
|
||||
trennt Modellstruktur sauber von den Daten (AbstractModel).
|
||||
* Linopy - beschriftete Arrays (xarray): eine Zeile Code erzeugt Tausende
|
||||
Nebenbedingungen auf einmal, ohne Python-Schleife.
|
||||
|
||||
Beide bringen KEINEN eigenen Solver mit; hier rechnet in beiden Faellen HiGHS.
|
||||
|
||||
WICHTIG: ortools wird in diesem Programm bewusst NICHT importiert - es
|
||||
vertraegt sich nicht mit der HiGHS-Kopie, die Pyomo und Linopy laden
|
||||
(siehe die Stolperfalle im Abschnitt 'Ein System - vier Programmieransaetze').
|
||||
|
||||
Benoetigt: pyomo, linopy, xarray, pandas, highspy, numpy
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyomo.environ as pyo
|
||||
import xarray as xr
|
||||
import linopy
|
||||
|
||||
ERWARTET = 530.0 # Ergebnis der Handrechnung
|
||||
|
||||
PRODUKTE = ["Standard", "Komfort", "Premium"]
|
||||
RESSOURCEN = ["Material", "Montage"]
|
||||
|
||||
DECKUNGSBEITRAG = np.array([10.0, 15.0, 25.0])
|
||||
VERBRAUCH = np.array([[1.0, 1.0, 2.0], # Material je Produkt
|
||||
[2.0, 3.0, 1.0]]) # Montage je Produkt
|
||||
VORRAT = np.array([40.0, 50.0])
|
||||
|
||||
|
||||
# --- Pyomo: algebraisch und indexbasiert ------------------------------------
|
||||
|
||||
def loese_mit_pyomo() -> tuple[float, list[float], float]:
|
||||
"""Pyomo denkt in Mengen und Indizes, wie ein Mathematiker es aufschreibt.
|
||||
|
||||
`Constraint(RESSOURCEN, rule=...)` erzeugt fuer JEDES Element der Menge
|
||||
eine Nebenbedingung - das ist das 'fuer alle i' der Formelsprache,
|
||||
unmittelbar in Code uebersetzt.
|
||||
"""
|
||||
t0 = time.perf_counter()
|
||||
|
||||
modell = pyo.ConcreteModel(name="Produktionsprogramm")
|
||||
modell.P = pyo.Set(initialize=PRODUKTE)
|
||||
modell.R = pyo.Set(initialize=RESSOURCEN)
|
||||
|
||||
modell.db = pyo.Param(modell.P, initialize=dict(zip(PRODUKTE, DECKUNGSBEITRAG)))
|
||||
modell.a = pyo.Param(modell.R, modell.P, initialize={
|
||||
(r, p): VERBRAUCH[i, j]
|
||||
for i, r in enumerate(RESSOURCEN) for j, p in enumerate(PRODUKTE)})
|
||||
modell.vorrat = pyo.Param(modell.R, initialize=dict(zip(RESSOURCEN, VORRAT)))
|
||||
|
||||
modell.x = pyo.Var(modell.P, domain=pyo.NonNegativeReals)
|
||||
|
||||
modell.ziel = pyo.Objective(
|
||||
expr=sum(modell.db[p] * modell.x[p] for p in modell.P),
|
||||
sense=pyo.maximize)
|
||||
|
||||
def kapazitaet(m, r):
|
||||
return sum(m.a[r, p] * m.x[p] for p in m.P) <= m.vorrat[r]
|
||||
|
||||
modell.kapazitaet = pyo.Constraint(modell.R, rule=kapazitaet)
|
||||
|
||||
ergebnis = pyo.SolverFactory("appsi_highs").solve(modell)
|
||||
dauer = time.perf_counter() - t0
|
||||
|
||||
status = ergebnis.solver.termination_condition
|
||||
if status != pyo.TerminationCondition.optimal:
|
||||
raise RuntimeError(f"Pyomo meldet Status: {status}")
|
||||
|
||||
return (float(pyo.value(modell.ziel)),
|
||||
[float(pyo.value(modell.x[p])) for p in PRODUKTE],
|
||||
dauer)
|
||||
|
||||
|
||||
# --- Linopy: beschriftete Arrays --------------------------------------------
|
||||
|
||||
def loese_mit_linopy() -> tuple[float, list[float], float]:
|
||||
"""Linopy denkt in beschrifteten Arrays (xarray).
|
||||
|
||||
Der entscheidende Unterschied: `(verbrauch * x).sum("produkt") <= vorrat`
|
||||
ist EINE Zeile und erzeugt so viele Nebenbedingungen, wie die Dimension
|
||||
'ressource' Eintraege hat. Bei 2 Ressourcen faellt das nicht auf, bei
|
||||
200 000 schon - dort entstehen sie als Matrixoperation statt in einer
|
||||
Python-Schleife.
|
||||
"""
|
||||
t0 = time.perf_counter()
|
||||
|
||||
# Benannte Indizes statt blosser Listen: Dadurch heissen die Achsen
|
||||
# 'produkt' und 'ressource', und xarray fuehrt sie beim Rechnen von allein
|
||||
# richtig zusammen. Ohne Namen vergibt linopy 'dim_0', und man muss
|
||||
# spaeter umbenennen - eine haeufige Stolperstelle.
|
||||
produkt = pd.Index(PRODUKTE, name="produkt")
|
||||
ressource = pd.Index(RESSOURCEN, name="ressource")
|
||||
|
||||
modell = linopy.Model()
|
||||
modell.add_variables(lower=0, coords=[produkt], name="menge")
|
||||
x = modell.variables["menge"]
|
||||
|
||||
db = xr.DataArray(DECKUNGSBEITRAG, coords=[produkt])
|
||||
verbrauch = xr.DataArray(VERBRAUCH, coords=[ressource, produkt])
|
||||
vorrat = xr.DataArray(VORRAT, coords=[ressource])
|
||||
|
||||
# EINE Zeile - sie erzeugt so viele Nebenbedingungen, wie die Achse
|
||||
# 'ressource' Eintraege hat. Genau das ist der Punkt.
|
||||
modell.add_constraints((verbrauch * x).sum("produkt") <= vorrat,
|
||||
name="kapazitaet")
|
||||
modell.add_objective((db * x).sum(), sense="max")
|
||||
|
||||
modell.solve(solver_name="highs", output_flag=False)
|
||||
dauer = time.perf_counter() - t0
|
||||
|
||||
if modell.termination_condition != "optimal":
|
||||
raise RuntimeError(f"Linopy meldet Status: {modell.termination_condition}")
|
||||
|
||||
loesung = modell.variables["menge"].solution.to_series()
|
||||
return (float(modell.objective.value),
|
||||
[float(loesung[p]) for p in PRODUKTE],
|
||||
dauer)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 80)
|
||||
print(" MODELLIERUNGSSCHICHTEN FUER GROSSE MODELLE")
|
||||
print("=" * 80)
|
||||
print(f"{'Schicht':<14} {'Z*':>10} {'Standard':>10} {'Komfort':>10} "
|
||||
f"{'Premium':>10} {'Zeit':>10}")
|
||||
print("-" * 80)
|
||||
|
||||
ergebnisse = []
|
||||
for name, loeser in [("Pyomo", loese_mit_pyomo), ("Linopy", loese_mit_linopy)]:
|
||||
ziel, mengen, dauer = loeser()
|
||||
ergebnisse.append(ziel)
|
||||
print(f"{name:<14} {ziel:>10.2f} {mengen[0]:>10.2f} {mengen[1]:>10.2f} "
|
||||
f"{mengen[2]:>10.2f} {dauer:>8.2f} s")
|
||||
|
||||
print("-" * 80)
|
||||
for ziel in ergebnisse:
|
||||
assert abs(ziel - ERWARTET) < 1e-6, \
|
||||
f"Abweichung von der Handrechnung: {ziel} statt {ERWARTET}"
|
||||
print(f"Beide stimmen mit der Handrechnung ueberein (Z* = {ERWARTET:.0f}).")
|
||||
print()
|
||||
print("Wann welche Schicht?")
|
||||
print(" Pyomo -> wenn Modellstruktur und Daten getrennt bleiben sollen,")
|
||||
print(" wenn nichtlineare Terme oder MINLP dazukommen koennen,")
|
||||
print(" wenn spaeter ein kommerzieller Solver angebunden wird.")
|
||||
print(" Linopy -> wenn die Daten ohnehin als beschriftete Arrays vorliegen")
|
||||
print(" (Energiesystem-, Netz- und Zeitreihenmodelle) und das")
|
||||
print(" Modell zehntausende gleichartige Nebenbedingungen hat.")
|
||||
print("=" * 80)
|
||||
Loading…
Reference in a new issue