Dein erster Batterie-Optimierer in Python

← Learning Hub Lektion 2 · Batterieoptimierungs-Kurs · Geschätzte Dauer: 20 Minuten

In Lektion 1 hast du die drei Teile eines Optimierungsproblems kennengelernt. Jetzt verwandeln wir diese Formulierung in lauffähigen Python-Code. Am Ende dieser Lektion hast du einen funktionierenden Optimierer, der aus Preisdaten einen 24-Stunden-Fahrplan erzeugt.

Werkzeuge: Pyomo + HiGHS

Wir brauchen zwei Dinge: eine Modellierungssprache um die Mathematik auszudrücken, und einen Solver um die optimale Lösung zu finden.

pip install pyomo highspy

Schritt 1: Parameter definieren

Verwenden wir realistische Day-Ahead-Preise (€/MWh) für einen typischen volatilen Tag. In der Produktion würdest du diese von EPEX SPOT oder deinem Datenanbieter abrufen.

import pyomo.environ as pyo
import numpy as np

# --- Parameter ---
T = 24  # Stunden
preise = [
    28, 25, 22, 20, 19, 21, 35, 55,    # Stunden 0-7  (Nacht günstig, Morgenrampe)
    62, 58, 52, 48, 45, 44, 47, 53,    # Stunden 8-15 (Mittags-Solardelle)
    68, 85, 92, 78, 60, 45, 35, 30     # Stunden 16-23 (Abendspitze dann Abfall)
]

P_max = 10.0    # MW — max. Lade-/Entladeleistung
E_max = 20.0    # MWh — Energiekapazität der Batterie
eta = 0.90      # Gesamtwirkungsgrad (Wurzel wird auf jede Richtung angewandt)
soc_init = 10.0 # MWh — Start bei 50% Ladezustand
Hinweis zum Wirkungsgrad: Wir teilen η in Ladewirkungsgrad (√η) und Entladewirkungsgrad (√η) auf. So ergibt Laden × √η × √η = Laden × η den korrekten Gesamtverlust. Manche Formulierungen wenden das volle η nur auf einer Seite an — beides ist gültig, nur konsequent bleiben.

Schritt 2: Das Pyomo-Modell aufbauen

# --- Modell ---
model = pyo.ConcreteModel()

# Mengen
model.T = pyo.RangeSet(0, T - 1)

# Entscheidungsvariablen
model.p_laden = pyo.Var(model.T, bounds=(0, P_max))
model.p_entladen = pyo.Var(model.T, bounds=(0, P_max))
model.soc = pyo.Var(model.T, bounds=(0, E_max))

# Zielfunktion: Erlös maximieren
eta_sqrt = np.sqrt(eta)

def erloesregel(m):
    return sum(
        preise[t] * (m.p_entladen[t] - m.p_laden[t])
        for t in m.T
    )
model.zielfunktion = pyo.Objective(rule=erloesregel, sense=pyo.maximize)

# Nebenbedingung: Energiebilanz (SoC-Dynamik)
def soc_regel(m, t):
    if t == 0:
        return m.soc[t] == soc_init + eta_sqrt * m.p_laden[t] - m.p_entladen[t] / eta_sqrt
    return m.soc[t] == m.soc[t-1] + eta_sqrt * m.p_laden[t] - m.p_entladen[t] / eta_sqrt

model.soc_bilanz = pyo.Constraint(model.T, rule=soc_regel)

Schritt 3: Lösen

# --- Lösen ---
solver = pyo.SolverFactory('highs')
ergebnis = solver.solve(model, tee=False)

# Solver-Status prüfen
print(f"Solver-Status: {ergebnis.solver.status}")
print(f"Optimaler Erlös: €{pyo.value(model.zielfunktion):.2f}")

Schritt 4: Ergebnisse extrahieren und anzeigen

import pandas as pd

# Fahrplan extrahieren
fahrplan = pd.DataFrame({
    'stunde': range(T),
    'preis_eur_mwh': preise,
    'laden_mw': [pyo.value(model.p_laden[t]) for t in range(T)],
    'entladen_mw': [pyo.value(model.p_entladen[t]) for t in range(T)],
    'soc_mwh': [pyo.value(model.soc[t]) for t in range(T)],
})
fahrplan['netto_mw'] = fahrplan['entladen_mw'] - fahrplan['laden_mw']

print(fahrplan.to_string(index=False))
Was du sehen wirst: Der Optimierer lädt in den billigsten Stunden (Stunden 3–5, ~€19–21/MWh) und entlädt während der Abendspitze (Stunden 16–18, ~€68–92/MWh). Dieser Spread ist dein Erlös.

Das vollständige Skript

"""
batterie_optimierer.py — Lektion 2: Day-Ahead-Fahrplan-Optimierer
"""
import pyomo.environ as pyo
import numpy as np
import pandas as pd

# --- Parameter ---
T = 24
preise = [28, 25, 22, 20, 19, 21, 35, 55,
          62, 58, 52, 48, 45, 44, 47, 53,
          68, 85, 92, 78, 60, 45, 35, 30]

P_max = 10.0    # MW
E_max = 20.0    # MWh
eta = 0.90      # Gesamtwirkungsgrad
soc_init = 10.0 # MWh

# --- Modell ---
model = pyo.ConcreteModel()
model.T = pyo.RangeSet(0, T - 1)

model.p_laden = pyo.Var(model.T, bounds=(0, P_max))
model.p_entladen = pyo.Var(model.T, bounds=(0, P_max))
model.soc = pyo.Var(model.T, bounds=(0, E_max))

eta_sqrt = np.sqrt(eta)

def erloesregel(m):
    return sum(preise[t] * (m.p_entladen[t] - m.p_laden[t]) for t in m.T)
model.zielfunktion = pyo.Objective(rule=erloesregel, sense=pyo.maximize)

def soc_regel(m, t):
    if t == 0:
        return m.soc[t] == soc_init + eta_sqrt * m.p_laden[t] - m.p_entladen[t] / eta_sqrt
    return m.soc[t] == m.soc[t-1] + eta_sqrt * m.p_laden[t] - m.p_entladen[t] / eta_sqrt
model.soc_bilanz = pyo.Constraint(model.T, rule=soc_regel)

# --- Lösen ---
solver = pyo.SolverFactory('highs')
ergebnis = solver.solve(model, tee=False)

print(f"Solver-Status: {ergebnis.solver.status}")
print(f"Optimaler Erlös: €{pyo.value(model.zielfunktion):.2f}\n")

# --- Ergebnisse ---
fahrplan = pd.DataFrame({
    'stunde': range(T),
    'preis': preise,
    'laden_mw': [round(pyo.value(model.p_laden[t]), 2) for t in range(T)],
    'entladen_mw': [round(pyo.value(model.p_entladen[t]), 2) for t in range(T)],
    'soc_mwh': [round(pyo.value(model.soc[t]), 2) for t in range(T)],
})
print(fahrplan.to_string(index=False))

Überprüfe dein Verständnis

Was erzeugt pyo.Var(model.T, bounds=(0, P_max))?

Wenn der Solver p_laden[4] = 10.0 und p_entladen[4] = 0.0 liefert, was passiert in Stunde 4?

Warum benutzen wir pyo.SolverFactory('highs') statt einen eigenen Suchalgorithmus zu schreiben?

Übungen (mache diese vor dem Weitergehen)

  1. Führe das Skript aus. Welchen Erlös findet der Optimierer?
  2. Ändere E_max von 20 auf 40 MWh. Steigt der Erlös? Warum?
  3. Setze eta = 1.0 (perfekter Wirkungsgrad). Wie viel mehr Erlös bringt eine verlustfreie Batterie?
  4. Setze soc_init = 0.0. Entlädt der Optimierer trotzdem am Abend? Schau dir die frühen Stunden an.

📖 Empfohlene Lektüre

Pyomo-Dokumentation: Einfache Beispiele — führt Schritt für Schritt durch das Erstellen von LP-Modellen. Lies das „Concrete Model"-Beispiel.

💬 Fragen? Wenn Pyomos Syntax verwirrend ist, der Solver unerwartete Ergebnisse liefert, oder du verstehen willst, was „tee=True" anzeigt — frag. Sich jetzt mit den Werkzeugen vertraut zu machen erspart später Schmerzen.

Rechtliches: Impressum