Gekoppelte Netze berechnen

Sie haben bereits gelernt, wie man Netze in pandapower und pandapipes aufsetzen und berechnen kann. Wesentliches Merkmal der Simulationsumgebung ist zudem die Möglichkeit, Netze miteinander zu koppeln, um analysieren zu können, wie sich der Zustand des einen auf den Zustand des anderen auswirkt. Um eine solche Berechnung durchzuführen, sind die folgenden Dinge vonnöten:

  • Die zwei zu verbindenden Netze
  • Das Hinzufügen von Kopplungselementen zwischen den Netzen. Dabei kann es sich bspw. um eine P2G-Anlage handeln. Kopplungselemente werden als Controller modelliert, da diese auch zur Regelung bestimmter Größen eingesetzt werden können. Controller existieren aber nicht nur zur Kopplung der Netze untereinander. Wir werden auch Fälle kennenlernen, in denen diese nur in einem Netz definiert sind.
  • Üblicherweise werden gekoppelte Netze immer über einen längeren Zeitraum beobachtet. Deswegen sehen wir uns auch an, wie man eine Zeitreihensimulation durchführen kann.

In diesem Tutorial wird eine P2G-Anlage und eine G2P-Einheit genutzt, um ein Strom- mit einem Gasnetz zu verbinden. Eingabewerte für diese Anlagen werden zu Beginn der Simulation festgelegt. Während der Simulation werden Ausgabegrößen anhand von Effizienzfaktoren berechnet.

Die Kopplung der Netze untereinenader erfolgt zunächst in drei Schritten. Eine Zeitreihenbetrachtung ist zunächst nicht enthalten, wird aufbauend auf der Kopplung aber hinzugefügt:

  1. Erzeugen eines Containers zur Aufnahme der zu koppelnden Netze
  2. Hinzufügen der Controller für die Kopplungselemente
  3. Durchführen der Berechnung

Erzeugen eines "Multi-Nets"

Im Gegensatz zu den bereits erstellten Netzwerken, machen wir uns diesmal nicht die Arbeit, Netze in der Konsole zu generieren. Stattdessen laden wir bereits vorhandene Netze einfach und definieren das Fluid.

In [ ]:
from pandapower import networks as e_nw
net_power = e_nw.example_simple()

import pandapipes as ppipes
from pandapipes import networks as g_nw

net_gas = g_nw.gas_meshed_square()
# some adjustments:
net_gas.junction.pn_bar = net_gas.ext_grid.p_bar = 30
net_gas.pipe.diameter_m = 0.4

# set fluid:
ppipes.create_fluid_from_lib(net_gas, 'hydrogen', overwrite=True)

Anschließend wird der "Multinet"-Container erstellt. Er nimmt die zu verbindenden Netzwerke im Rahmen einer gekoppelten Anaylse auf. Jedes Netz muss einen eigenen Namen zugewiesen bekommen. Standardnamen sind "power" und "gas", aber es kann jeder beliebige Name gewählt werden. Die Zahl der Netze ist nicht begrenzt.

In [ ]:
from pandapipes.multinet.create_multinet import create_empty_multinet, add_net_to_multinet
multinet = create_empty_multinet('tutorial_multinet')
add_net_to_multinet(multinet, net_power, 'power')
add_net_to_multinet(multinet, net_gas, 'gas')

Die einzelnen Netzwerke können über den Variablennamen oder den Multinet-Container angesprochen werden:

In [ ]:
print(multinet.nets['power'])
print(multinet.nets['gas'])
In [ ]:
print(net_power)
print(net_gas)
In [ ]:
print(net_power is multinet.nets['power'])
print(net_gas is multinet.nets['gas'])

Folglich werden Änderungen in den jeweils vom "Multinet" losgelösten Netzen auf die im "Multinet" übertragen.

Kopplungspunkte hinzufügen

Jetzt werden Elemente für die P2G und G2P-Controller hinzugefügt. Jeder Controller ist mit mindestens einem Element eines Netzwerks verbunden, von welchem er Elemente entnimmt oder dorthin überträgt. Im Falle von Kopplungspunkten für Multienergienetze gibt es zwei Verbindungen: Eine Verbindung zu einem Element des Gasnetzes und eine Verbindung zu einem Element des Stromnetzes.

Im Folgenden werden zunächst die Elemente erzeugt, mit denen die Controller verbunden werden:

In [ ]:
import pandapower as ppower
import pandapipes as ppipes

p2g_id_el = ppower.create_load(net_power, bus=3, p_mw=2, name="power to gas consumption")
p2g_id_gas = ppipes.create_source(net_gas, junction=1, mdot_kg_per_s=0, name="power to gas feed in")

g2p_id_gas = ppipes.create_sink(net_gas, junction=1, mdot_kg_per_s=0.1, name="gas to power consumption")
g2p_id_el = ppower.create_sgen(net_power, bus=5, p_mw=0, name="fuel cell feed in")

Jetzt werden die eigentlichen Controller erzeugt und initialisiert. Die Netzelemente, die mit den Controllern verbunden sind, werden als Parameter übergeben. Der Controller agiert damit als Kopplungspunkt zwischen den Netzen.

In [ ]:
from pandapipes.multinet.control.controller.multinet_control import P2GControlMultiEnergy, \
    G2PControlMultiEnergy

p2g_ctrl = P2GControlMultiEnergy(multinet, p2g_id_el, p2g_id_gas, efficiency=0.7,
                          name_power_net="power", name_gas_net="gas")

g2p_ctrl = G2PControlMultiEnergy(multinet, g2p_id_el, g2p_id_gas, efficiency=0.65,
                          name_power_net="power", name_gas_net="gas")

Intern arbeiten die Controller mit einem importierten Brennwert. Dieser stammt aus den Fluideigenschaften des Netzes

pandapipes/properties/[fluid_name]/higher_heating_value.txt)

Controller können auf vielfältige Weise eingesetzt werden. Alle Aspekte kann dieses Tutorial nicht abdecken. Weitere Infos finden Sie aber unter:https://pandapower.readthedocs.io/en/latest/control/control_loop.html

Simulation

Jetzt, wo die Netze und die Controller erstellt worden sind, kann die Berechnung gestartet werden. Es ist bekannt, dass die Berechnung von pandapower und pandapipes-Netzen mit den Kommandos runpp bzw. pipeflow gestartet wird. Werden gekoppelte Netze berechnet, so wird stattdessen der Befehl run_control eingesetzt, der intern die Berechnung der Teilnetze startet, aber auch dafür sorgt, dass die Controller aufgerufen werden.

In [ ]:
from pandapipes.multinet.control.run_control_multinet import run_control
run_control(multinet)

Nach der Berechnung wurden die Ausgabewerte aktualisiert und entsprechen der Eingangsleistung multipliziert mit dem Effizienzfaktor.

In [ ]:
print(net_gas.source.loc[p2g_id_gas, 'mdot_kg_per_s'])
print(net_power.sgen.loc[g2p_id_el, 'p_mw'])

Zusammengefasst:

In [ ]:
import pandapipes as ppipes
import pandapower as ppower

from pandapipes import networks as g_nw
from pandapower import networks as e_nw
from pandapipes.multinet.create_multinet import create_empty_multinet, add_net_to_multinet
from pandapipes.multinet.control.controller.multinet_control import P2GControlMultiEnergy, G2PControlMultiEnergy
from pandapipes.multinet.control.run_control_multinet import run_control

# get networks:
net_power = e_nw.example_simple()
net_gas = g_nw.gas_meshed_square()
# some adjustments:
net_gas.junction.pn_bar = net_gas.ext_grid.p_bar = 30
net_gas.pipe.diameter_m = 0.4
net_gas.controller.rename(columns={'controller': 'object'}, inplace=True) # due to new version

# set fluid:
fluid = {'name':'hydrogen', 'cal_value':38.4}
ppipes.create_fluid_from_lib(net_gas, fluid['name'], overwrite=True)

# create multinet and add networks:
multinet = create_empty_multinet('tutorial_multinet')
add_net_to_multinet(multinet, net_power, 'power')
add_net_to_multinet(multinet, net_gas, 'gas')

# create elements corresponding to conversion units:
p2g_id_el = ppower.create_load(net_power, bus=3, p_mw=2, name="power to gas consumption")
p2g_id_gas = ppipes.create_source(net_gas, junction=1, mdot_kg_per_s=0, name="power to gas feed in")

g2p_id_gas = ppipes.create_sink(net_gas, junction=1, mdot_kg_per_s=0.1, name="gas to power consumption")
g2p_id_el = ppower.create_sgen(net_power, bus=5, p_mw=0, name="fuel cell feed in")

# create coupling controllers:
p2g_ctrl = P2GControlMultiEnergy(multinet, p2g_id_el, p2g_id_gas, efficiency=0.7,
                                 name_power_net="power", name_gas_net="gas")

g2p_ctrl = G2PControlMultiEnergy(multinet, g2p_id_el, g2p_id_gas, efficiency=0.65,
                                 name_power_net="power", name_gas_net="gas")

# run simulation:
run_control(multinet)

Durchführung einer zeitabhängigen Simulation

In der Regel möchte man die Zustände des Systems für den Fall ermitteln, dass Eingabedaten mit der Zeit variieren. Dies kann dann der Fall sein, wenn z. B. Lasten ein zeitlich nicht konstantes Profil aufweisen. Die Controller, die wir im vorigen Abschnitt eingeführt haben, bilden selbst kein zeitabhängiges Verhalten ab. Sie können aber mit einem sogenennaten ConstController kombiniert werden, welche Zeitreihen einlesen und in jedem Zeitschritt einen anderen Wert zur Verfügung stellen kann. Es gibt Funktionen, welche die kombinierten Controller direkt erzeugen können. Die Namen dieser Funktionen sind coupled_p2g_const_control und coupled_g2p_const_control.

Das Beispiel des letzten Abschnitts wird jetzt um eine zeitabhängige Simulation erweitert. Der folgende Block richtet die Netze wieder ein. Noch fehlen allerdings die Controller.

In [ ]:
# prepare just like before
net_power = e_nw.example_simple()
net_gas = g_nw.gas_meshed_square()
net_gas.junction.pn_bar = net_gas.ext_grid.p_bar = 30
net_gas.pipe.diameter_m = 0.4
net_gas.controller.rename(columns={'controller': 'object'}, inplace=True) # due to new version
fluid = {'name':'hydrogen', 'cal_value':38.4}
ppipes.create_fluid_from_lib(net_gas, fluid['name'], overwrite=True)
multinet = create_empty_multinet('tutorial_multinet')
add_net_to_multinet(multinet, net_power, 'power_net')
add_net_to_multinet(multinet, net_gas, 'gas_net')

p2g_id_el = ppower.create_load(net_power, bus=3, p_mw=2, name="power to gas consumption")
p2g_id_gas = ppipes.create_source(net_gas, junction=1, mdot_kg_per_s=0, name="power to gas feed in")
g2p_id_gas = ppipes.create_sink(net_gas, junction=1, mdot_kg_per_s=0.1, name="gas to power consumption")
g2p_id_el = ppower.create_sgen(net_power, bus=5, p_mw=0, name="fuel cell feed in")

Der folgende Block erstellt eine Funktion, die Zufallsdaten für die Zeitreihen erzeugt. Insgesamt werden 10 Zeitschritte berechnet, wie am Parameter der Funktion zu erkennen ist. Der mit Zufallszahlen gefüllte pandas DataFrame wird am Ende der Funktion als Attribut eines Objekts der DFData-Klasse gespeichert. Diese wird von pandapower definiert und dient dem einfacheren Zugriff auf die im Frame gespeichertern Daten. Alle Controller können mit dieser Datenstruktur umgehen.

In [ ]:
from pandas import DataFrame
from numpy.random import random
from pandapower.timeseries import DFData

def create_data_source(n_timesteps=10):
    profiles = DataFrame()
    profiles['power to gas consumption'] = random(n_timesteps) * 2 + 1
    profiles['gas to power consumption'] = random(n_timesteps) * 0.1
    ds = DFData(profiles)

    return profiles, ds

profiles, ds = create_data_source(10)

Im Rahmen von zeitabhängigen Simulationen fallen größere Ergebnismengen an. Für jeden Zeitschritt kann der gesamte Zustand des Netzes gespeichert und anschließend ausgewertet werden. Für die Ergebnisse zeitabhängiger Simulationen wird eine weitere Datenstruktur bereitgestellt: Der OutputWriter. Auch bei diesem handelt es sich um eine Klasse.

Die folgende Funktion legt für jedes Teilnetz einen eigenen OutputWriter an und speichert diese in einem Python-dictionary. Für jedes Netz wird eine Liste auszugebener Größen, die log_variables, erstellt. Es können Spalten verschiedener Ergebnistabellen kombiniert werden. Die erstellten Listen werden anschließend im OutputWriter gespeichert.

In [ ]:
from os.path import join, dirname
from pandapower.timeseries import OutputWriter

def create_output_writers(multinet, time_steps=None):
    nets = multinet["nets"]
    ows = dict()
    for key_net in nets.keys():
        ows[key_net] = {}
        if isinstance(nets[key_net], ppower.pandapowerNet):
            log_variables = [('res_bus', 'vm_pu'),
                             ('res_line', 'loading_percent'),
                             ('res_line', 'i_ka'),
                             ('res_bus', 'p_mw'),
                             ('res_bus', 'q_mvar'),
                             ('res_load', 'p_mw'),
                             ('res_load', 'q_mvar')]
            ow = OutputWriter(nets[key_net], time_steps=time_steps,
                              log_variables=log_variables,
                              output_path=join(dirname('__file__'),'timeseries', 'results', 'power'),
                              output_file_type=".csv")
            ows[key_net] = ow
        elif isinstance(nets[key_net], ppipes.pandapipesNet):
            log_variables = [('res_sink', 'mdot_kg_per_s'),
                             ('res_source', 'mdot_kg_per_s'),
                             ('res_ext_grid', 'mdot_kg_per_s'),
                             ('res_pipe', 'v_mean_m_per_s'),
                             ('res_junction', 'p_bar'),
                             ('res_junction', 't_k')]
            ow = OutputWriter(nets[key_net], time_steps=time_steps,
                              log_variables=log_variables,
                              output_path=join(dirname('__file__'), 'timeseries', 'results', 'gas'),
                              output_file_type=".csv")
            ows[key_net] = ow
        else:
            raise AttributeError("Could not create an output writer for nets of kind " + str(key_net))
    return ows

ows = create_output_writers(multinet, 10)

Jetzt werden die bereits erwähnten Controller hinzugefügt. Es ist zu beachten, dass die data_source, welche die Zeitreihen beschreibt, als Parameter mit übergeben wird. So weiß der jeweilige Controller, woher er die Eingangsdaten des aktuellen Zeitschritts nehmen soll.

In [ ]:
from pandapipes.multinet.control.controller.multinet_control import coupled_p2g_const_control, \
    coupled_g2p_const_control
coupled_p2g_const_control(multinet, p2g_id_el, p2g_id_gas,
                          name_power_net="power_net", name_gas_net="gas_net",
                          profile_name='power to gas consumption', data_source=ds,
                          p2g_efficiency=0.7)
coupled_g2p_const_control(multinet, g2p_id_el, g2p_id_gas,
                          name_power_net="power_net", name_gas_net="gas_net",
                          element_type_power="sgen",
                          profile_name='gas to power consumption', data_source=ds,
                          g2p_efficiency=0.65)

Die ConstControllers werden in den Teilnetzen gespeichert. Die Kopplungscontroller, welche die Verbindung zwischen den Netzen herstellen, befinden sich dagegen im multinet.

In [ ]:
print(multinet.controller)
print(net_power.controller)
print(net_gas.controller)

Die Simulation wird mit dem Befehl run_timeseries gestartet. Zu beachten sind die Parameter der run_timeseries-Funktion. Sowohl die Zeitschrittweite, als auch die erstellte OutputWriter-Struktur wird der Funktion mit übergeben. Nach der Simulation kann auf die Outputwriter zugegriffen werden, um die gewünschten Größen zu extrahieren. Übriges: Innerhalb der run_timeseries-Funktion ruft pandapipes wieder die bereits bekannte run_control-Funktion auf. Im Wesentlichen wird nur eine Schleife um letztere Funktion gelegt, um die Berechnung für die angegebene Zahl von Zeitschritten zu wiederholen.

In [ ]:
from pandapipes.multinet.timeseries.run_time_series_multinet import run_timeseries
run_timeseries(multinet, time_steps=range(10), output_writers=ows)