operations_research/OR_HTML_04/programme/Modellierungsschichten.py

170 lines
6.4 KiB
Python
Raw Permalink Normal View History

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>
2026-09-08 01:20:09 +02:00
#!/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)