# -*- coding: utf-8 -*-
# Copyright 2016-2023 Flensburg University of Applied Sciences,
# Europa-Universität Flensburg,
# Centre for Sustainable Energy Systems,
# DLR-Institute for Networked Energy Systems
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# File description for read-the-docs
"""gas.py defines the methods to cluster gas grid networks
spatially for applications within the tool eTraGo."""
import logging
import os
from pypsa.clustering.spatial import (
aggregatebuses,
aggregateoneport,
busmap_by_kmeans,
)
from pypsa.components import Network
from six import iteritems
import numpy as np
import pandas as pd
import pypsa.io as io
if "READTHEDOCS" not in os.environ:
from etrago.cluster.spatial import (
drop_nan_values,
focus_weighting,
group_links,
kmedoids_dijkstra_clustering,
strategies_buses,
strategies_generators,
strategies_one_ports,
)
from etrago.tools.utilities import set_control_strategies
logger = logging.getLogger(__name__)
__copyright__ = (
"Flensburg University of Applied Sciences, "
"Europa-Universität Flensburg, "
"Centre for Sustainable Energy Systems, "
"DLR-Institute for Networked Energy Systems"
)
__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)"
__author__ = (
"MGlauer, MarlonSchlemminger, mariusves, BartelsJ, gnn, lukasoldi, "
"ulfmueller, lukasol, ClaraBuettner, CarlosEpia, KathiEsterl, "
"pieterhexen, fwitte, AmeliaNadal, cjbernal071421"
)
[docs]
def preprocessing(etrago, carrier, apply_on="grid_model"):
"""
Preprocesses the gas network data from the given Etrago object for the
spatial clustering process of the CH4 o H2 grid.
Parameters
----------
etrago : Etrago
An instance of the Etrago class
carrier : str
Name of the bus carrier that will be clustered
Returns
-------
None
Raises
------
ValueError
If "n_clusters_ch4" or "n_clusters_h2" is less than or equal to the
number of neighboring country gas buses.
"""
# Create network_gas (grid nodes in order to create the busmap basis)
network_gas = Network()
if apply_on == "grid_model":
network = etrago.network
elif apply_on == "market_model":
network = etrago.network_tsa
else:
logger.warning(
"""Parameter apply_on must be either 'grid_model' or 'market_model'
"""
)
buses_gas = network.buses.copy()
links_gas = network.links.copy()
io.import_components_from_dataframe(network_gas, buses_gas, "Bus")
network_gas.madd(
"Link", links_gas.index, **links_gas.loc[:, ~links_gas.isna().any()]
)
network_gas.buses["country"] = buses_gas.country
# Cluster buses
settings = etrago.args["network_clustering"]["gas_grids"]
if apply_on == "grid_model":
if carrier == "CH4":
total_clusters = settings["n_clusters_ch4"]
else:
total_clusters = settings["n_clusters_h2"]
else:
total_clusters = (
len(
buses_gas[
(buses_gas.carrier == carrier)
& (buses_gas.country != "DE")
]
)
+ 1
)
gas_filter = network_gas.buses["carrier"].values == carrier
num_neighboring_country = (
gas_filter & (network_gas.buses["country"] != "DE")
).sum()
network_gas.links = network_gas.links.loc[
network_gas.links["bus0"].isin(network_gas.buses.loc[gas_filter].index)
& network_gas.links["bus1"].isin(
network_gas.buses.loc[gas_filter].index
)
]
# select buses dependent on whether they should be clustered in
# (only DE or DE+foreign)
if etrago.args["network_clustering"]["method"]["per_country"]:
network_gas.buses = network_gas.buses.loc[
gas_filter & (network_gas.buses["country"].values == "DE")
]
if total_clusters <= num_neighboring_country:
msg = (
"The number of clusters for the gas sector ("
+ str(total_clusters)
+ ") must be higher than the number of neighboring country "
+ "gas buses ("
+ str(num_neighboring_country)
+ ")."
)
raise ValueError(msg)
n_clusters = total_clusters - num_neighboring_country
else:
network_gas.buses = network_gas.buses.loc[gas_filter]
n_clusters = total_clusters
def weighting_for_scenario(ch4_buses, save=None):
"""
Calculate CH4-bus weightings dependant on the connected
CH4-loads, CH4-generators and non-transport link capacities.
Stores are not considered for the clustering.
Parameters
----------
ch4_buses : pandas.DataFrame
Dataframe with CH4 etrago.network.buses to weight.
save : str or bool
Path to save weightings to as .csv
Returns
-------
weightings : pandas.Series
Integer weighting for each ch4_buses.index
"""
MAX_WEIGHT = 1e5 # relevant only for foreign nodes with extra high
# CH4 generation capacity
to_neglect = [
"CH4",
"H2_to_CH4",
"CH4_to_H2",
"H2_feedin",
]
# get all non-transport and non-H2 related links for each bus
rel_links = {}
for i in ch4_buses.index:
rel_links[i] = etrago.network.links.loc[
(
etrago.network.links.bus0.isin([i])
| etrago.network.links.bus1.isin([i])
)
& ~etrago.network.links.carrier.isin(to_neglect)
].index
# get all generators and loads related to ch4_buses
generators_ = pd.Series(
etrago.network.generators[
etrago.network.generators.carrier != "load shedding"
].index,
index=etrago.network.generators[
etrago.network.generators.carrier != "load shedding"
].bus,
)
buses_gas_gen = generators_.index.intersection(rel_links.keys())
loads_ = pd.Series(
etrago.network.loads.index, index=etrago.network.loads.bus
)
buses_gas_load = loads_.index.intersection(rel_links.keys())
# sum up all relevant entities and cast to integer
# Note: rel_links will hold the weightings for each bus afterwards
for i in rel_links:
rel_links[i] = etrago.network.links.loc[rel_links[i]].p_nom.sum()
if i in buses_gas_gen:
rel_links[i] += etrago.network.generators.loc[
generators_.loc[i]
].p_nom.sum()
if i in buses_gas_load:
rel_links[i] += (
etrago.network.loads_t.p_set.loc[:, loads_.loc[i]]
.mean()
.sum()
)
rel_links[i] = min(int(rel_links[i]), MAX_WEIGHT)
weightings = pd.DataFrame.from_dict(rel_links, orient="index")
if save:
weightings.to_csv(save)
return weightings
# weight buses for clustering
weight_gas = weighting_for_scenario(network_gas.buses, save=False)
return network_gas, weight_gas.squeeze(axis=1), n_clusters
[docs]
def kmean_clustering_gas(etrago, network_ch4, weight, n_clusters):
"""
Performs K-means clustering on the gas network data in the given
`network_ch4` pypsa.Network object.
Parameters
----------
etrago : Etrago
An instance of the Etrago class
network_ch4 : pypsa.Network
A Network object containing the gas network data.
weight : str or None
The name of the bus weighting column to use for clustering. If None,
unweighted clustering is performed.
n_clusters : int
The number of clusters to create.
Returns
-------
busmap : pandas.Series
A pandas.Series object mapping each bus in the CH4 network to its
corresponding cluster ID
None
None is returned because k-means clustering makes no use of medoids
"""
settings = etrago.args["network_clustering"]["method"]
busmap = busmap_by_kmeans(
network_ch4,
bus_weightings=weight,
n_clusters=n_clusters,
n_init=settings["n_init"],
max_iter=settings["max_iter"],
tol=settings["tol"],
random_state=settings["random_state"],
)
return busmap, None
[docs]
def get_h2_clusters(etrago, busmap_ch4):
"""
Maps H2 buses to CH4 cluster IDds and creates unique H2 cluster IDs.
Parameters
----------
etrago : Etrago
An instance of the Etrago class
busmap_ch4 : pd.Series
A Pandas Series mapping each bus in the CH4 network to its
corresponding cluster ID.
Returns
-------
busmap : pd.Series
A Pandas Series mapping each bus in the combined CH4 and H2 network
to its corresponding cluster ID.
"""
# Mapping of H2 buses to new CH4 cluster IDs
busmap_h2 = pd.Series(
busmap_ch4.loc[etrago.ch4_h2_mapping.index].values,
index=etrago.ch4_h2_mapping.values,
)
# Create unique H2 cluster IDs
n_gas = etrago.args["network_clustering"]["gas_grids"]["n_clusters_ch4"]
busmap_h2 = (busmap_h2.astype(int) + n_gas).astype(str)
busmap_h2 = busmap_h2.squeeze()
busmap = pd.concat([busmap_ch4, busmap_h2])
return busmap
[docs]
def sector_coupled_clustering_strategy(etrago):
"""
Defines clustering strategies for sectors without a grid per scenario.
Parameters
----------
etrago : Etrago
An instance of the Etrago class
Returns
-------
strategy : dict
Dictionary containing cluster strategies for each sector.
"""
if "eGon2035" in etrago.args["scn_name"]:
if etrago.args["method"]["distribution_grids"]:
strategy = {
"central_heat": {
"base": ["CH4", "AC"],
"strategy": "simultaneous",
},
"H2_grid": {"base": ["CH4"], "strategy": "consecutive"},
"H2_saltcavern": {
"base": ["H2_grid"],
"strategy": "consecutive",
},
}
else:
strategy = {
"central_heat": {
"base": ["CH4", "AC"],
"strategy": "simultaneous",
},
"rural_heat": {"base": ["AC"], "strategy": "consecutive"},
"H2_grid": {"base": ["CH4"], "strategy": "consecutive"},
"H2_saltcavern": {
"base": ["H2_grid"],
"strategy": "consecutive",
},
"Li_ion": {"base": ["AC"], "strategy": "consecutive"},
}
elif "eGon100RE" in etrago.args["scn_name"]:
strategy = {
"central_heat": {
"base": ["CH4", "AC"],
"strategy": "simultaneous",
},
"rural_heat": {
"base": ["CH4", "AC"],
"strategy": "simultaneous",
},
"H2": {
"base": ["CH4"],
"strategy": "consecutive",
},
"H2_saltcavern": {
"base": ["H2_grid"],
"strategy": "consecutive",
},
"Li_ion": {
"base": ["AC"],
"strategy": "consecutive",
},
}
elif "powerd" in etrago.args["scn_name"]:
strategy = {
"central_heat": {"base": ["CH4"], "strategy": "consecutive"},
"rural_heat": {"base": ["CH4", "AC"], "strategy": "simultaneous"},
"H2": {"base": ["CH4"], "strategy": "consecutive"},
"H2_saltcavern": {"base": ["H2_grid"], "strategy": "consecutive"},
"Li_ion": {"base": ["AC"], "strategy": "consecutive"},
}
elif "status2019" in etrago.args["scn_name"]:
strategy = {
"central_heat": {"base": ["CH4"], "strategy": "consecutive"},
"rural_heat": {"base": ["CH4", "AC"], "strategy": "simultaneous"},
"H2": {"base": ["CH4"], "strategy": "consecutive"},
}
else:
strategy = {
"central_heat": {
"base": ["CH4", "AC"],
"strategy": "simultaneous",
},
"rural_heat": {"base": ["AC"], "strategy": "consecutive"},
"H2_grid": {"base": ["CH4"], "strategy": "consecutive"},
"H2_saltcavern": {"base": ["H2_grid"], "strategy": "consecutive"},
"Li_ion": {"base": ["AC"], "strategy": "consecutive"},
}
logger.warning(f"""
No strategy defined for sector coupled clustering in scenario
{etrago.args['scn_name']}
Using default values instead - please check if they are correct:
{strategy}
""")
return strategy
[docs]
def gas_postprocessing(
etrago,
busmap,
medoid_idx=None,
apply_on="grid_model",
aggregate_generators_carriers=None,
):
"""
Performs the postprocessing for the gas grid clustering based on the
provided busmap
and returns the clustered network.
Parameters
----------
etrago : Etrago
An instance of the Etrago class
busmap : pd.Series
A Pandas Series mapping each bus to its corresponding cluster ID.
medoid_idx : pd.Series
A pandas.Series object containing the medoid indices for the gas
network.
Returns
-------
network_gasgrid_c : pypsa.Network
A pypsa.Network containing the clustered network.
busmap : pd.Series
A Pandas Series mapping each bus to its corresponding cluster ID.
"""
settings = etrago.args["network_clustering"]["gas_grids"]
scn = etrago.args["scn_name"]
if apply_on == "grid_model":
if settings["k_ch4_busmap"] is False:
if (
etrago.args["network_clustering"]["method"]["algorithm"]
== "kmeans"
):
busmap.index.name = "bus_id"
busmap.name = "cluster"
busmap.to_csv(
"kmeans_gasgrid_busmap_"
+ str(settings["n_clusters_ch4"])
+ "_result.csv"
)
else:
busmap.name = "cluster"
busmap_ind = pd.Series(
medoid_idx[busmap.values.astype(int)].values,
index=busmap.index,
dtype=pd.StringDtype(),
)
busmap_ind.name = "medoid_idx"
export = pd.concat([busmap, busmap_ind], axis=1)
export.index.name = "bus_id"
export.to_csv(
"kmedoids-dijkstra_gasgrid_busmap_"
+ str(settings["n_clusters_ch4"])
+ "_result.csv"
)
network = etrago.network
else:
network = etrago.pre_market_model
if ("H2_grid" in network.buses.carrier.unique()) & (scn in ["eGon2035"]):
busmap = get_h2_clusters(etrago, busmap)
# Add all other buses to busmap
missing_idx = list(
network.buses[(~network.buses.index.isin(busmap.index))].index
)
next_bus_id = highestInteger(network.buses.index) + 1
new_gas_buses = [str(int(x) + next_bus_id) for x in busmap]
busmap_idx = list(busmap.index) + missing_idx
busmap_values = new_gas_buses + missing_idx
busmap = pd.Series(busmap_values, index=busmap_idx)
for name, data in sector_coupled_clustering_strategy(etrago).items():
strategy = data["strategy"]
if strategy == "consecutive":
busmap_sector_coupling = consecutive_sector_coupling(
network,
busmap,
data["base"],
name,
)
elif strategy == "simultaneous":
if len(data["base"]) < 2:
msg = (
"To apply simultaneous clustering for the "
+ name
+ " buses, at least 2 base buses must be selected."
)
raise ValueError(msg)
busmap_sector_coupling = simultaneous_sector_coupling(
network,
busmap,
data["base"],
name,
)
else:
msg = (
"Strategy for sector coupled clustering must be either "
"'consecutive' or 'coupled'."
)
raise ValueError(msg)
for key, value in busmap_sector_coupling.items():
busmap.loc[key] = value
busmap = busmap.astype(str)
busmap.index = busmap.index.astype(str)
network_gasgrid_c = get_clustering_from_busmap(
network,
busmap,
aggregate_generators_carriers,
generator_strategies=strategies_generators(),
one_port_strategies=strategies_one_ports(),
bus_strategies=strategies_buses(),
)
if apply_on != "market_model":
# aggregation of the links and links time series
network_gasgrid_c.links, network_gasgrid_c.links_t = group_links(
network_gasgrid_c
)
# Overwrite p_nom of links with carrier "H2_feedin" (eGon2035 only)
if etrago.args["scn_name"] == "eGon2035":
H2_energy_share = 0.05053 # H2 energy share via volumetric share
# outsourced in a mixture of H2 and CH4 with 15 %vol share
feed_in = network_gasgrid_c.links.loc[
network_gasgrid_c.links.carrier == "H2_feedin"
]
pipeline_capacities = network_gasgrid_c.links.loc[
network_gasgrid_c.links.carrier == "CH4"
]
for bus in feed_in["bus1"].values:
# calculate the total pipeline capacity connected to a specific bus
nodal_capacity = pipeline_capacities.loc[
(pipeline_capacities["bus0"] == bus)
| (pipeline_capacities["bus1"] == bus),
"p_nom",
].sum()
# multiply total pipeline capacity with H2 energy share
# corresponding to volumetric share
network_gasgrid_c.links.loc[
(network_gasgrid_c.links["bus1"].values == bus)
& (network_gasgrid_c.links["carrier"].values == "H2_feedin"),
"p_nom",
] = (
nodal_capacity * H2_energy_share
)
# Insert components not related to the gas clustering
other_components = ["Line", "StorageUnit", "ShuntImpedance", "Transformer"]
for c in network.iterate_components(other_components):
io.import_components_from_dataframe(
network_gasgrid_c,
c.df,
c.name,
)
for attr, df in c.pnl.items():
if not df.empty:
io.import_series_from_dataframe(
network_gasgrid_c,
df,
c.name,
attr,
)
io.import_components_from_dataframe(
network_gasgrid_c, network.carriers, "Carrier"
)
network_gasgrid_c.consistency_check()
network_gasgrid_c.determine_network_topology()
# Adjust x and y coordinates of 'CH4' and 'H2_grid' medoids
if (
etrago.args["network_clustering"]["method"]["algorithm"]
== "kmedoids-dijkstra"
and len(medoid_idx) > 0
):
for cluster in medoid_idx:
network_gasgrid_c.buses.loc[busmap[cluster], "x"] = (
network.buses.loc[cluster, "x"]
)
network_gasgrid_c.buses.loc[busmap[cluster], "y"] = (
network.buses.loc[cluster, "y"]
)
drop_nan_values(network_gasgrid_c)
return (network_gasgrid_c, busmap)
[docs]
def highestInteger(potentially_numbers):
"""Fetch the highest number of a series with mixed types
Parameters
----------
potentially_numbers : pandas.Series
Series with mixed dtypes, potentially containing numbers.
Returns
-------
highest : int
Highest integer found in series.
"""
highest = 0
for number in potentially_numbers:
try:
num = int(number)
if num > highest:
highest = num
except ValueError:
pass
return highest
[docs]
def simultaneous_sector_coupling(
network, busmap, carrier_based, carrier_to_cluster
):
"""
Cluster sector coupling technology based on multiple connected carriers.
The topology of the sector coupling technology must be in a way, that the
links connected to other sectors do only point inwards. E.g. for the heat
sector, heat generating technologies from electricity or gas only point to
the heat sector and not vice-versa.
Parameters
----------
network : pypsa.Network
PyPSA network instance.
busmap : pandas.Series
Series with lookup table for clustered buses.
carrier_based : list
Carriers on which the clustering of the sector coupling is based.
carrier_to_cluster : str
Name of the carrier which should be clustered
Returns
-------
dict
Busmap for the sector coupling cluster.
"""
next_bus_id = highestInteger(busmap.values) + 1
buses_clustered = network.buses[
network.buses["carrier"].isin(carrier_based)
]
buses_to_cluster = network.buses[
network.buses["carrier"] == carrier_to_cluster
]
buses_to_skip = network.buses[
network.buses["carrier"] == carrier_to_cluster + "_store"
]
connected_links = network.links.loc[
network.links["bus0"].isin(buses_clustered.index)
& network.links["bus1"].isin(buses_to_cluster.index)
& ~network.links["bus1"].isin(buses_to_skip.index)
& ~network.links["bus0"].isin(buses_to_skip.index)
]
busmap = busmap.to_dict()
connected_links["bus0_clustered"] = (
connected_links["bus0"].map(busmap).fillna(connected_links["bus0"])
)
connected_links["bus1_clustered"] = (
connected_links["bus1"].map(busmap).fillna(connected_links["bus1"])
)
# cluster sector coupling technologies
busmap = sc_multi_carrier_based(buses_to_cluster, connected_links)
busmap = {
bus_id: bus_num + next_bus_id for bus_id, bus_num in busmap.items()
}
# cluster appedices
skipped_links = network.links.loc[
(
network.links["bus1"].isin(buses_to_skip.index)
& network.links["bus0"].isin(buses_to_cluster.index)
)
| (
network.links["bus0"].isin(buses_to_cluster.index)
& network.links["bus1"].isin(buses_to_skip.index)
)
]
# map skipped buses after clustering
skipped_links["bus0_clustered"] = (
skipped_links["bus0"].map(busmap).fillna(skipped_links["bus0"])
)
skipped_links["bus1_clustered"] = (
skipped_links["bus1"].map(busmap).fillna(skipped_links["bus1"])
)
busmap_series = pd.Series(busmap)
next_bus_id = highestInteger(busmap_series.values) + 1
# create clusters for skipped buses
clusters = busmap_series.unique()
for i in range(len(clusters)):
buses = skipped_links.loc[
skipped_links["bus0_clustered"] == clusters[i], "bus1_clustered"
]
for bus_id in buses:
busmap[bus_id] = next_bus_id + i
buses = skipped_links.loc[
skipped_links["bus1_clustered"] == clusters[i], "bus0_clustered"
]
for bus_id in buses:
busmap[bus_id] = next_bus_id + i
return busmap
[docs]
def consecutive_sector_coupling(
network, busmap, carrier_based, carrier_to_cluster
):
"""
Cluster sector coupling technology based on single connected carriers.
The topology of the sector coupling technology must be in a way, that the
links connected to other sectors do only point inwards. E.g. for the heat
sector, heat generating technologies from electricity or gas only point to
the heat sector and not vice-versa.
Parameters
----------
network : pypsa.Network
PyPSA network instance.
busmap : pandas.Series
Series with lookup table for clustered buses.
carrier_based : list
Carriers on which the clustering of the sector coupling is based.
carrier_to_cluster : str
Name of the carrier which should be clustered
Returns
-------
busmap_sc : dict
Busmap for the sector coupled cluster.
"""
next_bus_id = highestInteger(busmap.values) + 1
buses_to_skip = network.buses[
network.buses["carrier"] == carrier_to_cluster + "_store"
]
buses_to_cluster = network.buses[
network.buses["carrier"] == carrier_to_cluster
]
buses_clustered = network.buses[
network.buses["carrier"] == carrier_based[0]
]
busmap_sc = {}
for base in carrier_based:
# remove already clustered buses
buses_to_cluster = buses_to_cluster[
~buses_to_cluster.index.isin(busmap_sc.keys())
]
buses_clustered = network.buses[network.buses["carrier"] == base]
connected_links = network.links.loc[
network.links["bus0"].isin(buses_clustered.index)
& network.links["bus1"].isin(buses_to_cluster.index)
& ~network.links["bus1"].isin(buses_to_skip.index)
& ~network.links["bus0"].isin(buses_to_skip.index)
]
connected_links["bus0_clustered"] = (
connected_links["bus0"].map(busmap).fillna(connected_links["bus0"])
)
connected_links["bus1_clustered"] = (
connected_links["bus1"].map(busmap).fillna(connected_links["bus1"])
)
# cluster sector coupling technologies
busmap_by_base = sc_single_carrier_based(connected_links)
bus_num = 0
for bus_id, bus_num in busmap_by_base.items():
busmap_by_base[bus_id] = bus_num + next_bus_id
next_bus_id = bus_num + next_bus_id + 1
busmap_sc.update(busmap_by_base)
buses_to_cluster = buses_to_cluster[
~buses_to_cluster.index.isin(busmap_sc.keys())
]
if len(buses_to_cluster) > 0:
msg = "The following buses are not added to any cluster: " + str(
buses_to_cluster.index
)
logger.warning(msg)
# cluster appedices
skipped_links = network.links.loc[
(
network.links["bus1"].isin(buses_to_skip.index)
& network.links["bus0"].isin(busmap_sc.keys())
)
| (
network.links["bus0"].isin(busmap_sc.keys())
& network.links["bus1"].isin(buses_to_skip.index)
)
]
# map skipped buses after clustering
skipped_links["bus0_clustered"] = (
skipped_links["bus0"].map(busmap_sc).fillna(skipped_links["bus0"])
)
skipped_links["bus1_clustered"] = (
skipped_links["bus1"].map(busmap_sc).fillna(skipped_links["bus1"])
)
busmap_series = pd.Series(busmap_sc)
next_bus_id = highestInteger(busmap_series.values) + 1
# create clusters for skipped buses
clusters = busmap_series.unique()
for i in range(len(clusters)):
buses = skipped_links.loc[
skipped_links["bus0_clustered"] == clusters[i], "bus1_clustered"
]
for bus_id in buses:
busmap_sc[bus_id] = next_bus_id + i
buses = skipped_links.loc[
skipped_links["bus1_clustered"] == clusters[i], "bus0_clustered"
]
for bus_id in buses:
busmap_sc[bus_id] = next_bus_id + i
return busmap_sc
[docs]
def sc_multi_carrier_based(buses_to_cluster, connected_links):
"""
Create busmap for sector coupled carrier based on multiple other carriers.
Parameters
----------
buses_to_cluster : pandas.Series
Series containing the buses of the sector coupled carrier which are
to be clustered.
connected_links : pandas.DataFrame
Links that connect from the buses with other carriers to the
buses of the sector coupled carrier.
Returns
-------
busmap : dict
Busmap for the sector coupled carrier.
"""
clusters = pd.Series()
for bus_id in buses_to_cluster.index:
clusters.loc[bus_id] = tuple(
sorted(
connected_links.loc[
connected_links["bus1_clustered"] == bus_id,
"bus0_clustered",
].unique()
)
)
duplicates = clusters.unique()
busmap = {}
for i in range(len(duplicates)):
cluster = clusters[clusters == duplicates[i]].index.tolist()
if len(cluster) > 1:
busmap.update({bus: i for bus in cluster})
return busmap
[docs]
def sc_single_carrier_based(connected_links):
"""
Create busmap for sector coupled carrier based on single other carrier.
Parameters
----------
connected_links : pandas.DataFrame
Links that connect from the buses with other carrier to the
buses of the sector coupled carrier.
Returns
-------
busmap : dict
Busmap for the sector coupled carrier.
"""
busmap = {}
clusters = connected_links["bus0_clustered"].unique()
for i in range(len(clusters)):
buses = connected_links.loc[
connected_links["bus0_clustered"] == clusters[i], "bus1_clustered"
].unique()
busmap.update({bus: i for bus in buses})
return busmap
[docs]
def get_clustering_from_busmap(
network,
busmap,
aggregate_generators_carriers=None,
line_length_factor=1.0,
with_time=True,
bus_strategies=dict(),
one_port_strategies=dict(),
generator_strategies=dict(),
):
"""
Aggregates components of the given network based on a bus mapping and
returns a clustered gas grid pypsa.Network.
Parameters
----------
network : pypsa.Network
The input pypsa.Network object
busmap : pandas.Sereies :
A mapping of buses to clusters
line_length_factor : float
A factor used to adjust the length of new links created during
aggregation. Default is 1.0.
with_time : bool
Determines whether to copy the time-dependent properties of the input
network to the output network. Default is True.
bus_strategies : dict
A dictionary of custom strategies to use during the aggregation step.
Default is an empty dictionary.
one_port_strategies : dict
A dictionary of custom strategies to use during the one-port component
aggregation step. Default is an empty dictionary.
Returns
-------
network_gasgrid_c : pypsa.Network
A new gas grid pypsa.Network object with aggregated components based
on the bus mapping.
"""
network_gasgrid_c = Network()
# Aggregate buses
new_buses = aggregatebuses(
network,
busmap,
custom_strategies=bus_strategies,
)
new_buses.index.name = "bus_id"
io.import_components_from_dataframe(network_gasgrid_c, new_buses, "Bus")
if with_time:
network_gasgrid_c.set_snapshots(network.snapshots)
network_gasgrid_c.snapshot_weightings = (
network.snapshot_weightings.copy()
)
# Aggregate one port components
one_port_components = ["Generator", "Load", "Store"]
for one_port in one_port_components:
if one_port != "Generator":
new_df, new_pnl = aggregateoneport(
network,
busmap,
component=one_port,
with_time=with_time,
custom_strategies=one_port_strategies.get(one_port, {}),
)
else:
new_df, new_pnl = aggregateoneport(
network,
busmap,
carriers=aggregate_generators_carriers,
component=one_port,
with_time=with_time,
custom_strategies=generator_strategies,
)
io.import_components_from_dataframe(
network_gasgrid_c, new_df, one_port
)
for attr, df in iteritems(new_pnl):
io.import_series_from_dataframe(
network_gasgrid_c, df, one_port, attr
)
# Aggregate links
new_links = (
network.links.assign(
bus0=network.links.bus0.map(busmap),
bus1=network.links.bus1.map(busmap),
)
.dropna(subset=["bus0", "bus1"])
.loc[lambda df: df.bus0 != df.bus1]
)
# preparation for CH4 pipeline aggregation:
# pipelines are treated differently compared to other links, since all of
# them will be considered bidirectional. That means, if a pipeline exists,
# that connects one cluster with a different one simultaneously with a
# pipeline that connects these two clusters in reversed order (e.g. bus0=1,
# bus1=12 and bus0=12, bus1=1) they are aggregated to a single pipeline.
# therefore, the order of bus0/bus1 is adjusted
pipeline_mask = new_links["carrier"] == "CH4"
sorted_buses = np.sort(
new_links.loc[pipeline_mask, ["bus0", "bus1"]].values, 1
)
new_links.loc[pipeline_mask, ["bus0", "bus1"]] = sorted_buses
# import the links and the respective time series with the bus0 and bus1
# values updated from the busmap
io.import_components_from_dataframe(
network_gasgrid_c, new_links.loc[:, ~new_links.isna().all()], "Link"
)
if with_time:
for attr, df in network.links_t.items():
if not df.empty:
filtered_df = df[df.columns.intersection(new_links.index)]
io.import_series_from_dataframe(
network_gasgrid_c, filtered_df, "Link", attr
)
return network_gasgrid_c
[docs]
def join_busmap_medoids(
busmap1: pd.Series,
busmap2: pd.Series,
medoid_idx1: pd.Series,
medoid_idx2: pd.Series,
):
"""
Parameters
----------
busmap1 : pd.Series
busmap2 : pd.Series
medoid_idx1 : pd.Series
medoid_idx2 : pd.Series
Returns
-------
busmap : pd.Series
Pandas series joining busmap1 and busmap2
medoid_idx : pd.Series
Pandas series joining medoid_idx1 and medoid_idx2
"""
length_m1 = len(medoid_idx1)
medoid_idx2.index = medoid_idx2.index + length_m1
busmap2 = (busmap2.apply(int) + length_m1).apply(str)
busmap = pd.concat([busmap1, busmap2])
medoid_idx = pd.concat([medoid_idx1, medoid_idx2])
busmap = busmap.apply(int).map(medoid_idx)
medoid_idx.index = medoid_idx.astype(int)
return busmap, medoid_idx
[docs]
def run_spatial_clustering_gas(self):
"""
Performs spatial clustering on the gas network using either K-means or
K-medoids-Dijkstra algorithm. Updates the network topology by aggregating
buses and links, and then performs postprocessing to finalize the changes.
Returns
--------
None
Raises
-------
ValueError: If the selected method is not "kmeans" or "kmedoids-dijkstra".
"""
if ("CH4" in self.network.buses.carrier.values) | (
"H2_grid" in self.network.buses.carrier.values
):
settings = self.args["network_clustering"]["gas_grids"]
if settings["active"]:
method = self.args["network_clustering"]["method"]["algorithm"]
logger.info(f"Start {method} clustering GAS")
ch4_network, weight_ch4, n_clusters_ch4 = preprocessing(
self, "CH4"
)
if "H2_grid" in self.network.links.carrier.unique():
h2_network, weight_h2, n_clusters_h2 = preprocessing(
self, "H2_grid"
)
focus_region = self.args["network_clustering"]["method"][
"focus_region"
]
if focus_region:
weight_ch4 = focus_weighting(
self,
ch4_network,
weight_ch4,
focus_region,
cluster_within=self.args["network_clustering"][
"gas_grids"
]["cluster_within_focus"],
per_country=self.args["network_clustering"]["method"][
"per_country"
],
cpu_cores=self.args["network_clustering"]["method"][
"cpu_cores"
],
)
if "H2_grid" in self.network.links.carrier.unique():
weight_h2 = focus_weighting(
self,
h2_network,
weight_h2,
focus_region,
cluster_within=self.args["network_clustering"][
"gas_grids"
]["cluster_within_focus"],
per_country=self.args["network_clustering"]["method"][
"per_country"
],
cpu_cores=self.args["network_clustering"]["method"][
"cpu_cores"
],
)
if method == "kmeans":
if settings["k_ch4_busmap"]:
busmap = pd.read_csv(
settings["k_ch4_busmap"],
index_col="bus_id",
dtype=pd.StringDtype(),
).squeeze()
medoid_idx = None
else:
busmap_ch4, medoid_idx_ch4 = kmean_clustering_gas(
self, ch4_network, weight_ch4, n_clusters_ch4
)
if "H2_grid" in self.network.links.carrier.unique():
busmap_h2, medoid_idx_h2 = kmean_clustering_gas(
self, h2_network, weight_h2, n_clusters_h2
)
elif method == "kmedoids-dijkstra":
if settings["k_ch4_busmap"]:
busmap = pd.read_csv(
settings["k_ch4_busmap"],
index_col="bus_id",
dtype=pd.StringDtype(),
)
medoid_idx = pd.Series(
busmap["medoid_idx"].unique(),
index=busmap["cluster"].unique(),
dtype=pd.StringDtype(),
)
busmap = busmap["cluster"]
else:
busmap_ch4, medoid_idx_ch4 = kmedoids_dijkstra_clustering(
self,
ch4_network.buses,
ch4_network.links,
weight_ch4,
n_clusters_ch4,
)
if "H2_grid" in self.network.links.carrier.unique():
(
busmap_h2,
medoid_idx_h2,
) = kmedoids_dijkstra_clustering(
self,
h2_network.buses,
h2_network.links,
weight_h2,
n_clusters_h2,
)
else:
msg = (
'Please select "kmeans" or "kmedoids-dijkstra" as '
"spatial clustering method for the gas network"
)
raise ValueError(msg)
if "H2_grid" in self.network.links.carrier.unique():
busmap, medoid_idx = join_busmap_medoids(
busmap_ch4, busmap_h2, medoid_idx_ch4, medoid_idx_h2
)
else:
busmap = busmap_ch4
medoid_idx = medoid_idx_ch4
self.network, busmap = gas_postprocessing(self, busmap, medoid_idx)
self.update_busmap(busmap)
# The control parameter is overwritten in pypsa's clustering.
# The function network.determine_network_topology is called,
# which sets slack bus(es).
set_control_strategies(self.network)
logger.info(
"""CH4 Network clustered to {} DE-buses and {} foreign buses
with {} algorithm.""".format(
len(
self.network.buses.loc[
(self.network.buses.carrier == "CH4")
& (self.network.buses.country == "DE")
]
),
len(
self.network.buses.loc[
(self.network.buses.carrier == "CH4")
& (self.network.buses.country != "DE")
]
),
method,
)
)
if "H2_grid" in self.network.links.carrier.unique():
logger.info(
"""H2 Network clustered to {} DE-buses and {} foreign buses
with {} algorithm.""".format(
len(
self.network.buses.loc[
(self.network.buses.carrier == "H2_grid")
& (self.network.buses.country == "DE")
]
),
len(
self.network.buses.loc[
(self.network.buses.carrier == "H2_grid")
& (self.network.buses.country != "DE")
]
),
method,
)
)