Einfaches Beispiel “Strom und Wärme”

Eine einfache Illustration, in der wir verschiedene Anlagen verbinden:

  • KWK Anlage

  • P2H (z.B. Wärmepumpe, Geothermie)

  • Wärmespeicher

… plus (festem) Wärmebedarf und Strompreise

Das Wärmenetz besteht aus “Nord”, “Süd” plus Verbindung

Ein paar Vorbereitungen in Python

[1]:
import eaopack as eao
import pandas as pd
import datetime as dt
import matplotlib.pyplot as plt

Definition der “Knoten”, also der grundsätzlichen Struktur

Knoten bilden den virtuellen Punkt, in dem Wärme- und Stromanlagen angeschlossen sind. Die Knoten können durch “Transporte” verbunden werden

[2]:
# Definition der phys. Einheiten
unit_power      = eao.assets.Unit(volume = 'MWh(el)', flow = 'MW(el)')
unit_heat       = eao.assets.Unit(volume = 'MWh(th)', flow = 'MW(th)')
# Definition der Knoten
node_power      = eao.assets.Node(name = 'strom'      , unit = unit_power)
node_heat_nord  = eao.assets.Node(name = 'waerme_nord', unit = unit_heat)
node_heat_sued  = eao.assets.Node(name = 'waerme_sued', unit = unit_heat)

Definition der einzelnen Anlagen und Verträge

Notiz: Wir vermeiden eine grundsätzliche Unterscheidung zwischen phys. Anlagen und Verträgen. Z.B.

  • der Wärmebedarf kann auch als zu erfüllender Absatzvertrag gesehen werden

  • es könnten auch Strom-Terminverträge eingebunden werden (so physisch erfüllt)

[3]:

# (1) Wärmespeicher storage = eao.assets.Storage(name = 'speicher', nodes = node_heat_nord, cap_out = .5, cap_in = .5, size = 3., start_level = 0.25, end_level = 0.25, eff_in = 0.9, block_size = 'd') # Speicher soll täglich neu optimiert werden (nicht "leer hinterlassen") # (2) Power to Heat Anlage (die Wärmepumpe). Hier vereinfacht abgebildet # Wir können zu einem gegebenen Wirkungsgrad "Strom in Wärme tauschen" power2heat = eao.assets.MultiCommodityContract(name = 'P2H', # Wärmepumpe (z.B. Geothermie) min_cap = 0, max_cap = 2, nodes = [node_power, node_heat_nord], # Lokalisierung: Knoten Nord für Wärme - plus Stromknoten factors_commodities=[-1, 2]) # Wirkundsgrad: 1 MWh Strom zu 2 MWh Wärme # (3) Kraft-Wärme-Kopplung (KWK). Hier als GuD abgebildet # Achtung - hier sind einige Parameter als Standardwerte gesetzt, die je nach Anwendungsfall angepasst werden sollten KWK = eao.assets.CHPAsset(name = "KWK", min_cap = 0, max_cap = 5, # max. Leistung Strom nodes = [node_power, node_heat_sued], # Erzeugung von Strom und Wärme in die entsprechenden Netze, bei Bedarf auch Gas start_costs = 0, # im Beispiel keine Startkosten extra_costs = 25, # Brennstoffkosten in €/MWh, natürlich auch als Zeitreihe möglich conversion_factor_power_heat=0.5, # Verhältnis Produktion Wärme zu Strom 2:1, d.h. 1MW Stromverlust bei 2MW Wärmeproduktion max_share_heat = .5) # max. Anteil der Wärme an Gesamtproduktion (in MWh) ### ..viele weitere Parameter möglich, siehe Dokumentation. Z.B. Rampen, Mindestlaufzeiten, etc. # (4) Wärmebedarf # Der Wärmebedarf muss immer gedeckt werden. Hier als "Contract" abgebildet; Maximum = Minimum bedeutet "keine Flexibilität" bedarf_nord = eao.assets.Contract(name = "bedarf_nord", min_cap = "waerme_bedarf_nord", # Referenziert auf Bezeichnung in Datenquelle max_cap = "waerme_bedarf_nord", nodes = node_heat_nord) bedarf_sued = eao.assets.Contract(name = "bedarf_sued", min_cap = "waerme_bedarf_sued", max_cap = "waerme_bedarf_sued", nodes = node_heat_sued) # (5) Wärmeübertragungsnetz. Hier als "Transport" Asset abgebildet # Generell wird der Fluss immer in eine Richtung definiert. Daher im Beispiel 2 einzelne Richtungen Süd->Nord und Nord->Süd netz_nord_sued = eao.assets.Transport(name = 'netz_ns', max_cap = 2, # max. Wärmeübertragungskapazität nodes = [node_heat_nord, node_heat_sued]) netz_sued_nord = eao.assets.Transport(name = 'netz_sn', max_cap = 1.5, nodes = [node_heat_sued, node_heat_nord]) # (3) Strommarkt # Wir können praktisch unbegrenzt Strom kaufen und verkaufen - zum vorgegebenen Preis market = eao.assets.SimpleContract(name = 'strommarkt', price='strompreis', min_cap= -1000, max_cap=1000, nodes = node_power)

Portfolio zusammensetzen

Die Anlagen und Verträge bilden die Details ab. Mit der Information zu den Knoten können sie einfach im Portfolio zusammengefasst werden

  • Szenarioanalyse mit und ohne Anlagen

  • auch viele Einzel-Portfolien möglich, falls nur Teile modelliert werden

  • Teil-Portfolien können bei Bedarf als eine Anlage im Gesamtmodell zusammengefasst werden

[4]:
portf        = eao.portfolio.Portfolio([storage,
                                        power2heat,
                                        KWK,
                                        market,
                                        bedarf_nord,
                                        bedarf_sued,
                                        netz_nord_sued,
                                        netz_sued_nord])

Graphische Darstellung

Automatisch generiert. Im GUI kann die Darstellung angepasst werden.

  • Gelb: Knoten

  • Grau: Assets

  • Pfeile: Anbindung der Assets an Knoten oder Transport

[5]:
eao.network_graphs.create_graph(portf = portf, title = 'Beispiel: Wärmenetz mit KWK, Wärmepumpe und Speicher')
../../_images/samples_power_and_heat_power_and_heat_sample_10_0.png

Randbemerkung

Das EAO ist so aufgesetzt, dass es einfach an eigene Datenbanken und Systeme angebunden werden kann. Z.B. können alle Daten einfach in JSON exportiert (und eingelesen) werden. So auch im GUI

[6]:
## in JSON File speichern zur weiteren Verwendung, z.B. in eigener Datenbank, dem GUI, etc. Genauso für Assets, etc.
eao.serialization.to_json(portf, 'portf.json')      # Gesamtbeispiel
eao.serialization.to_json(storage, 'speicher.json')
# ... etc. für alle Assets möglich'

Daten laden

Hier: Preise und Wärmebedarf aus Excel Datei laden

[7]:
data = pd.read_excel("2020_sample_daten.xlsx")
data.set_index('datum', inplace=True)
data.round(2).head()
[7]:
strompreis waerme_bedarf_nord waerme_bedarf_sued
datum
2020-01-01 00:00:00 41.88 -2.11 -0.31
2020-01-01 01:00:00 38.60 -2.17 -0.39
2020-01-01 02:00:00 36.55 -2.20 -0.49
2020-01-01 03:00:00 32.32 -2.20 -0.59
2020-01-01 04:00:00 30.85 -2.17 -0.70

Optimierung durchführen

[8]:
Start = dt.date(2020,1,1)
End   = dt.date(2020,1,3)
tg    = eao.assets.Timegrid(Start, End, freq = 'h')     # hier: stündlich optimieren
out   = eao.optimize(portf = portf, timegrid = tg, data = data)
### Solver nach Geschmack leicht austauschbar. Z.B. Gurobi, CPLEX, freie Solver: SCIP, HIGHS ...
# out = eao.optimize(portf = portf, timegrid = tg, data = data, solver = 'SCIP')
### Split problem
out   = eao.optimize(portf = portf, timegrid = tg, data = data)
# out   = eao.optimize(portf = portf, timegrid = tg, data = data, split_interval_size='MS')

Randnotiz “Performance und Größenbeschränkung”:

  • MIP und LP: Kann das Portfolio als “LP” gelöst werden, ist die Rechenzeit deutlich schneller, als bei einem MIP. Hier kann über den Parameter “make_soft_problem” in der Optimierung gesteuert werden

  • Solver sind nach Geschmack leicht austauschbar. Z.B. Gurobi, CPLEX, freie Solver: SCIP, HIGHS …

  • Generell erzeugen Speicher in der Optimierung leicht schwer lösbare Probleme. Hier empfehlen wir, die “block_size” z.B. auf täglich/wöchentlich zu setzen. So wird der Speicher nur über innerhalb jeden Tages / jeder Woche optimiert

  • “Split Optimization”: Für die meisten Probleme ist es eine gute Näherung, das Problem für jede Woche/ jeden Monat etc. zu lösen. In der entsprechenden Variante setzt EAO die Lösung automatisch zusammen. Randbedingungen wie Speicher Level oder Mindestmengen werden heruntergebrochen

Resultate analysieren

Vereinfachte Darstellung. Im Normalfall Export nach Excel oder in eine Schnittstelle in Datenbanken

[9]:
out['summary'] # Zusammenfassung. Gesantwert in € (Deckungsbeitrag)
[9]:
Values
Parameter
status successful
value 1267.861623
[10]:
out['DCF'].transpose().iloc[:,0:2].round(2) # Detailsicht auf die Cash Flows (auch diskontierte Optimierung möglich (DCF / Maximierung des NPV))
[10]:
2020-01-01 00:00:00 2020-01-01 01:00:00
speicher 0.00 0.00
P2H 0.00 0.00
KWK -125.00 -125.00
strommarkt 164.02 143.65
bedarf_nord 0.00 0.00
bedarf_sued 0.00 0.00
netz_ns 0.00 0.00
netz_sn 0.00 0.00
[11]:
# Detailsicht auf die Betriebsweise der Anlagen (in MWh)
print(out['dispatch'].transpose().iloc[:,0:2].round(2))
                           2020-01-01 00:00:00  2020-01-01 01:00:00
speicher (waerme_nord)                    0.25                 0.00
P2H (strom)                              -0.73                -0.90
P2H (waerme_nord)                         1.46                 1.80
KWK (strom)                               4.65                 4.62
KWK (waerme_sued)                         0.70                 0.76
strommarkt (strom)                       -3.92                -3.72
bedarf_nord (waerme_nord)                -2.11                -2.17
bedarf_sued (waerme_sued)                -0.31                -0.39
netz_ns (waerme_nord)                    -0.60                -0.61
netz_ns (waerme_sued)                     0.60                 0.61
netz_sn (waerme_sued)                    -0.99                -0.98
netz_sn (waerme_nord)                     0.99                 0.98
[12]:
# Detailsicht auf die EIngangsgrößen (in €/MWh bzw. MW)  [hier vor Allem die vorgegebenen Preise, andere Analysen möglich --> Kosten für die Wärme pro Stunde]
out['prices'].transpose().iloc[:,0:2].round(2)
[12]:
2020-01-01 00:00:00 2020-01-01 01:00:00
nodal price: waerme_nord 20.94 19.30
nodal price: strom 41.88 38.60
nodal price: waerme_sued 20.94 19.30
input data: strompreis 41.88 38.60
input data: waerme_bedarf_nord -2.11 -2.17
input data: waerme_bedarf_sued -0.31 -0.39

Darstellung in Graphiken – zur Diskussion

[13]:
df = pd.merge(out['dispatch'], out['prices'], left_index=True, right_index=True, how='left')

heat = ['P2H (waerme_nord)',
        'KWK (waerme_sued)',
        'speicher (waerme_nord)']
power = ['P2H (strom)',
         'KWK (strom)']

fig, ax = plt.subplots(1,3, tight_layout = True, figsize=(12,4))
df[heat].plot(ax = ax[0], style = '-')
df[power].plot(ax = ax[1], style = '-')
df['input data: strompreis'].plot(ax = ax[2], style = '-')
ax[0].set_title('Wärme')
ax[1].set_title('Strom')
ax[2].set_title('Strompreis')
plt.show()

../../_images/samples_power_and_heat_power_and_heat_sample_24_0.png