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:
dschlueter 2026-09-08 01:20:09 +02:00
commit b7af2f1d9a
468 changed files with 262141 additions and 0 deletions

View 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)