Tutorial on how to couple a power grid and a gas network by a power-to-gas plant and a fuel cell

In this tutorial, a power network and a gas network are coupled by a power-to-gas unit (P2G) and a gas-to-power unit (G2P), e.g. a fuel cell. The P2G and G2P have an input value that is set in one network (power or gas consumption, respectively). During the simulation, the output value is calculated by applying efficiency factors and is written then to the other network.

There are three basic steps:

  1. bringing the networks together in a multinet-frame
  2. adding elements for the P2G and G2P units and coupling controller
  3. executing the coupled power and pipe flow

Creating a multi-net

First, we import some example networks and set the fluid for the gas net and P2G conversion.

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)

Then, we create a 'multinet'. It serves as a container for multiple networks to enable coupled simulation. Each net in the multinet has to have an unique name. Any name can be chosen - default names are 'power' and 'gas', but 'net1' and 'net2' would work just as fine. The number of networks in the multinet is not limited.

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

The individual networks can be called from the multinet or by the variable name - the result is identical:

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'])

Thus, changes to the networks will be found at both places.

Add elements that represent the coupling units

Now, we add elements to represent the input and output of the P2G and G2P units. They are assigned to specific buses / junctions. The input values have to be set. Since the output is calculated during the simulation, we can simply set it to 0 when calling the create function.

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

Now, the coupling controllers are imported and initialized. We hand over the IDs of the P2G unit in the power grid (i.e., which load element represents the electrolyser) and in the gas grid (i.e., which source elements represents the P2G feed-in). Analogously, a G2P controller is created.

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

Internally, both controllers calculate with the higher heating value of the gas. (It is a a property of the gas ('fluid') in the gas network and provided in the file pandapipes/properties/[fluid_name]/higher_heating_value.txt)

It is also possible to order the controllers hierarchical, (cf. the Control chapter in the pandapower documentation)

Run simulation

Now, the simulation can be run. As there are different run functions required (power flow or pipe flow), we simply execute run_control for the multinet. This collects all nets and controllers and conducts the corresponding run function.

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

Now, the output values have been updated and equal the power input times efficiency (and consideration of unit conversion):

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

In summary:

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:
ppipes.create_fluid_from_lib(net_gas, 'hydrogen', 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)

Time series simulation

Sometimes, the input values (and the corresponding outputs) for conversion units change over time, e.g. during a time series simulation. The MultiEnergy controllers themselves cannot handle time series inputs. However, they can easily be combined with a ConstController that updates the input values according to a time series. After the update of the values, the MultiEnergy controller is executed to calculate and write the output value to the other net. The convenience functions to create both controllers in one step are coupled_p2g_const_control and coupled_g2p_const_control.

Here is an example for a coupled time series simulation. First, the nets are prepared like before:

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
ppipes.create_fluid_from_lib(net_gas, 'hydrogen', 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")

For the time series, some example data is created and defined as data source.

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)

Then, output writers are create for the time series simulation:

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)

Now, we add the aforementioned combined controllers.

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)

The ConstControllers are stored in the separate nets, while the coupling controllers can be found in the multinet:

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

The time series is calculated with a run_timeseries function that has been adapted for multinets:

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