Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to merge each isolated bus to the backbone network #903

Merged
merged 76 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
b992acd
Add a function to fetch isolated nodes to network
ekatef Oct 18, 2023
f389d02
Apply fetch-isolated
ekatef Oct 18, 2023
ea0eb88
Add import
ekatef Oct 18, 2023
a399ae1
Add import
ekatef Oct 18, 2023
e8262b8
Generalize sub-setting of an array
ekatef Oct 26, 2023
52eb9a9
Improve a message
ekatef Oct 26, 2023
1af3d41
Remove comments
ekatef Oct 26, 2023
4aabc91
Merge remote-tracking branch 'origin/fetch_isolated_to_network' into …
ekatef Oct 26, 2023
6391b21
Add a return on condition in case there is no isolated nodes
ekatef Oct 26, 2023
bc8b755
Merge branch 'main' into fetch_isolated_to_network
ekatef Oct 27, 2023
a92a8fc
Clarify a comment
ekatef Oct 31, 2023
2ad6fa6
Merge remote-tracking branch 'origin/fetch_isolated_to_network' into …
ekatef Oct 31, 2023
4d46fe5
Add a TODO comment
ekatef Nov 2, 2023
e641447
Add filtering by carrier for the network to merge into
ekatef Dec 7, 2023
febe2a7
Remove a not-necessary comment
ekatef Dec 7, 2023
dc9be87
Improve implementation of buses merging
ekatef Dec 8, 2023
06349a9
Merge branch 'main' into fetch_isolated_to_network
ekatef Jan 1, 2024
dd3a039
Wrap-up into a function transformation of a buses dataframe into geop…
ekatef Jan 1, 2024
831182b
Fix selection of the buses to be merged with
ekatef Jan 1, 2024
813ce0a
Read CRS from the network
ekatef Jan 1, 2024
aa0f079
Re-implement the spatial transformation function
ekatef Jan 1, 2024
6743186
Improve naming
ekatef Jan 1, 2024
b79b364
Add bus_id column
ekatef Jan 3, 2024
4af66f0
Add a function to identify isolated networks
ekatef Jan 3, 2024
b8d127e
Handle isolated networks
ekatef Jan 3, 2024
2460f02
Fix threshold
ekatef Jan 3, 2024
dc75850
Remove lines corresponding to the isolated buses
ekatef Jan 3, 2024
1f7cc78
Exclude population raster from demand distribution
ekatef Jan 1, 2024
1430c66
Account for the carriers
ekatef Jan 4, 2024
1031926
Hardcode CRS
ekatef Jan 4, 2024
085ee0c
Revert "Exclude population raster from demand distribution"
ekatef Jan 4, 2024
3c9b651
Fix a typo in a comment
ekatef Jan 16, 2024
4e45ecb
Add a fetch threshold to the config
ekatef Jan 16, 2024
c103064
Improve docstrings
ekatef Jan 16, 2024
16ff175
Fix a fetch threshold
ekatef Jan 16, 2024
bd71663
Fix CRS input
ekatef Jan 16, 2024
fef6935
Switch-on filtering by load
ekatef Jan 16, 2024
db139ad
Improve naming
ekatef Jan 16, 2024
2bf9704
Revise finding isolated networks to respect national partition
ekatef Feb 8, 2024
453583c
Add a country column to the buses dataframe
ekatef Feb 8, 2024
7ad0853
Move filtering to the find-isolated function
ekatef Feb 8, 2024
9efcdc6
Re-factor filtering of backbone buses
ekatef Feb 8, 2024
92f9d69
Account for geography when finding the closest buses
ekatef Feb 8, 2024
222ab30
Revise threshold configuration parameters
ekatef Feb 8, 2024
d1ef441
Code clean-up
ekatef Feb 8, 2024
d4a212b
Avoid global variables
ekatef Feb 8, 2024
a0e9c87
Fix mapping
ekatef Feb 8, 2024
71c398d
Avoid merging into multi-country networks
ekatef Feb 8, 2024
b167bf0
Use an absolute power threshold
ekatef Feb 9, 2024
9e3466c
Merge branch 'main' into fetch_isolated_to_network
ekatef Mar 18, 2024
3656788
Remove a redundant column
ekatef Mar 18, 2024
fd6354e
Improve comments
ekatef Mar 18, 2024
4046495
Merge branch 'pypsa-meets-earth:main' into fetch_isolated_to_network
ekatef Mar 26, 2024
f8cb01f
Add load data to the geo-transform function
ekatef Mar 30, 2024
b1697a0
Update definitions of the geo-dataframes
ekatef Mar 30, 2024
fc97649
Update configuration files
ekatef Mar 30, 2024
4ee2604
Merge remote-tracking branch 'origin/fetch_isolated_to_network' into …
ekatef Mar 30, 2024
60ae7cd
Remove an outdated function
ekatef Mar 30, 2024
b7f50c5
Fix formatting
ekatef Mar 30, 2024
f2a7840
Keep the backbone block together
ekatef Mar 30, 2024
f00c04b
Add a comment
ekatef Mar 30, 2024
1702c3a
Fix code structure
ekatef Mar 30, 2024
9b725cb
Fix filtering for floats
ekatef Mar 30, 2024
f1ec53b
Revise naming
ekatef Mar 30, 2024
6cf958b
Keep id column
ekatef Mar 30, 2024
3953b70
Add a safety filtering
ekatef Mar 30, 2024
a3086f7
Revise config defaults
ekatef Mar 30, 2024
10cad77
Add an entry to the configtables
ekatef Mar 30, 2024
171bd00
Add a release note
ekatef Mar 30, 2024
6f20a31
Revise naming
ekatef Mar 30, 2024
75019ac
Merge branch 'main' into fetch_isolated_to_network
ekatef Apr 7, 2024
ad8d083
Remove not needed imports
ekatef Apr 7, 2024
278bc80
Implement Davide's suggestion
ekatef Apr 7, 2024
cc97b02
Use a safer filtering
ekatef Apr 7, 2024
f92638e
Add a release note
ekatef Apr 8, 2024
a9b6f59
Merge branch 'main' into fetch_isolated_to_network
ekatef Apr 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ cluster_options:
remove_stubs: true
remove_stubs_across_borders: true
p_threshold_drop_isolated: 20 # [MW] isolated buses are being discarded if bus mean power is below the specified threshold
p_threshold_merge_isolated: 300 # [MW] isolated buses are being merged into a single isolated bus if bus mean power is below the specified threshold
p_threshold_merge_isolated: 300 # [MW] isolated buses are being merged into a single isolated bus if a bus mean power is below the specified threshold
s_threshold_fetch_isolated: 0.05 # [-] a share of the national load for merging an isolated network into a backbone network
cluster_network:
algorithm: kmeans
feature: solar+onwind-time
Expand Down
3 changes: 2 additions & 1 deletion config.tutorial.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ cluster_options:
remove_stubs: true
remove_stubs_across_borders: true
p_threshold_drop_isolated: 20 # [MW] isolated buses are being discarded if bus mean power is below the specified threshold
p_threshold_merge_isolated: 300 # [MW] isolated buses are being merged into a single isolated bus if bus mean power is below the specified threshold
p_threshold_merge_isolated: 300 # [MW] isolated buses are being merged into a single isolated bus if a bus mean power is below the specified threshold
s_threshold_fetch_isolated: 0.05 # [-] a share of the national load for merging an isolated network into a backbone network
cluster_network:
algorithm: kmeans
feature: solar+onwind-time
Expand Down
1 change: 1 addition & 0 deletions doc/configtables/cluster_options.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ simplify_network,,,
-- remove_stubs_across_borders, bool, "{True, False}", "True: Stub lines and links can be removed across borders."
-- p_threshold_drop_isolated, MW, positive number, "Isolated buses are discarded if bus mean power is below the `p_threshold_drop_isolated`."
-- p_threshold_merge_isolated, MW, positive number, "Isolated buses are merged into a single isolated bus if bus mean power is below `p_threshold_merge_isolated`."
-- s_threshold_fetch_isolated, [-], positive number, "Isolated networks are merged into a backbone network of a respective country if the network load comprises a share of the national load less than p_threshold_fetch_isolated."
cluster_network,,,
-- algorithm,,"{hac, kmeans}", "Clustering algorithm used in the cluster_network rule. Options available are Hierarchical Agglomerative Clustering (HAC) or k-means."
-- feature,,"Str in the format ‘carrier1+carrier2+...+carrierN-X’, where CarrierI can be from {‘solar’, ‘onwind’, ‘offwind’, ‘ror’} and X is one of {‘cap’, ‘time’}. Examples: solar+offwind-cap, solar-time", "Only for Hierarchical Agglomerative Clustering (HAC). Feature(s) used to do the clustering."
Expand Down
2 changes: 2 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ E.g. if a new rule becomes available describe how to use it `snakemake -j1 run_t

* Generalize line types for AC and DC networks. `PR #999 <https://github.com/pypsa-meets-earth/pypsa-earth/pull/999>`__

* Add an option to merge isolated networks into respective backbone networks by countries. `PR #903 <https://github.com/pypsa-meets-earth/pypsa-earth/pull/903>`__

**Minor Changes and bug-fixing**

* Minor bug-fixing to get the generalised line types work for DC lines. `PR #1008 <https://github.com/pypsa-meets-earth/pypsa-earth/pull/1008>`__
Expand Down
150 changes: 146 additions & 4 deletions scripts/simplify_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
import sys
from functools import reduce

import geopandas as gpd
import numpy as np
import pandas as pd
import pypsa
Expand Down Expand Up @@ -743,6 +744,136 @@ def drop_isolated_nodes(n, threshold):
return n


def transform_to_gdf(n, network_crs):
buses_df = n.buses.copy()
buses_df["bus_id"] = buses_df.index

# load data are crucial to deal with sub-networks
buses_df = buses_df.join(n.loads_t.p_set.sum().T.rename("load"))
buses_df["load_in_subnetw"] = buses_df.groupby(["country", "sub_network"])[
"load"
].transform("sum")
buses_df["load_in_country"] = buses_df.groupby(["country"])["load"].transform("sum")
buses_df["sbntw_share_of_country_load"] = buses_df.apply(
lambda row: (
row.load_in_subnetw / row.load_in_country if row.load_in_country > 0 else 0
),
axis=1,
)
buses_df["is_backbone_sbntw"] = (
buses_df.groupby(["country"], as_index=True)[
"sbntw_share_of_country_load"
].transform("max")
<= buses_df["sbntw_share_of_country_load"]
)

gdf_buses = gpd.GeoDataFrame(
buses_df,
geometry=gpd.points_from_xy(buses_df.x, buses_df.y),
crs=network_crs,
)
return gdf_buses


def merge_into_network(n, threshold, aggregation_strategies=dict()):
"""
Find isolated AC nodes and sub-networks in the network and merge those of
them which have load value and a number of buses below than the specified
thresholds into a backbone network.

Parameters
----------
n : PyPSA.Network
Original network
threshold : float
Load power used as a threshold to merge isolated nodes
aggregation_strategies: dictionary
Functions to be applied to calculate parameters of the aggregated grid

Returns
-------
modified network
"""
# keep original values of the overall load and generation in the network
# to track changes due to drop of buses
generators_mean_origin = n.generators.p_nom.mean()
load_mean_origin = n.loads_t.p_set.mean().mean()

network_crs = snakemake.params.geo_crs

n.determine_network_topology()

n_buses_gdf = transform_to_gdf(n, network_crs=network_crs)

# do not merge sub-networks spanned through a number of countries
n_buses_gdf["is_multicnt_subntw"] = n_buses_gdf.sub_network.map(
n_buses_gdf.groupby(["sub_network"]).country.nunique() > 1
)

gdf_islands = (
n_buses_gdf.query("~is_multicnt_subntw")
.query("carrier=='AC'")
.query("sbntw_share_of_country_load < @threshold")
)
# return the original network if no isolated nodes are detected
if len(gdf_islands) == 0:
return n, n.buses.index.to_series()

gdf_backbone_buses = n_buses_gdf.query("is_backbone_sbntw").query("carrier=='AC'")

# find the closest buses of the backbone networks for each isolated network and each country
islands_bcountry = {k: d for k, d in gdf_islands.groupby("country")}
gdf_map = (
gdf_backbone_buses.query("country in @islands_bcountry")
.groupby("country")
.apply(lambda d: gpd.sjoin_nearest(islands_bcountry[d["country"].values[0]], d))
)
nearest_bus_df = n.buses.loc[n.buses.index.isin(gdf_map.bus_id_right)]

i_lines_islands = n.lines.loc[n.lines.bus1.isin(gdf_islands.index)].index
n.mremove("Line", i_lines_islands)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This remove should not be needed and performed by the clustering no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally yes, but our use case is not well captured by get_clustering_from_busmap( ), as it seems to expect that the lines in a cluster belong to the same sub-network. When I try to remove the line n.mremove("Line", i_lines_islands) leads to AssertionError In Bus cluster sub_network the values of attribute sub_network do not agree.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok :)


isolated_buses_mapping = (
gdf_map[["bus_id_right"]].droplevel("country").to_dict()["bus_id_right"]
)

busmap = (
n.buses.index.to_series()
.replace(isolated_buses_mapping)
.astype(str)
.rename("busmap")
)

# return the original network if no changes are detected
if (busmap.index == busmap).all():
return n, n.buses.index.to_series()

bus_strategies, generator_strategies = get_aggregation_strategies(
aggregation_strategies
)

clustering = get_clustering_from_busmap(
n,
busmap,
bus_strategies=bus_strategies,
aggregate_generators_weighted=True,
aggregate_generators_carriers=None,
aggregate_one_ports=["Load", "StorageUnit"],
line_length_factor=1.0,
generator_strategies=generator_strategies,
scale_link_capital_costs=False,
)

load_mean_final = n.loads_t.p_set.mean().mean()
generators_mean_final = n.generators.p_nom.mean()

logger.info(
f"Fetched {len(gdf_islands)} isolated buses into the network. Load attached to a single bus with discrepancies of {(100 * ((load_mean_final - load_mean_origin)/load_mean_origin)):2.1E}% and {(100 * ((generators_mean_final - generators_mean_origin)/generators_mean_origin)):2.1E}% for load and generation capacity, respectively"
)

return clustering.network, busmap


def merge_isolated_nodes(n, threshold, aggregation_strategies=dict()):
"""
Find isolated nodes in the network and merge those of them which have load
Expand All @@ -755,6 +886,8 @@ def merge_isolated_nodes(n, threshold, aggregation_strategies=dict()):
Original network
threshold : float
Load power used as a threshold to merge isolated nodes
aggregation_strategies: dictionary
Functions to be applied to calculate parameters of the aggregated grid

Returns
-------
Expand All @@ -775,19 +908,19 @@ def merge_isolated_nodes(n, threshold, aggregation_strategies=dict()):

# isolated buses with load below than a specified threshold should be merged
i_load_islands = n.loads_t.p_set.columns.intersection(i_islands)
i_suffic_load = i_load_islands[
i_islands_merge = i_load_islands[
n.loads_t.p_set[i_load_islands].mean(axis=0) <= threshold
]

# all the nodes to be merged should be mapped into a single node
map_isolated_node_by_country = (
n.buses.assign(bus_id=n.buses.index)
.loc[i_suffic_load]
.loc[i_islands_merge]
.groupby("country")["bus_id"]
.first()
.to_dict()
)
isolated_buses_mapping = n.buses.loc[i_suffic_load, "country"].replace(
isolated_buses_mapping = n.buses.loc[i_islands_merge, "country"].replace(
map_isolated_node_by_country
)
busmap = (
Expand Down Expand Up @@ -821,7 +954,7 @@ def merge_isolated_nodes(n, threshold, aggregation_strategies=dict()):
generators_mean_final = n.generators.p_nom.mean()

logger.info(
f"Merged {len(i_suffic_load)} buses. Load attached to a single bus with discrepancies of {(100 * ((load_mean_final - load_mean_origin)/load_mean_origin)):2.1E}% and {(100 * ((generators_mean_final - generators_mean_origin)/generators_mean_origin)):2.1E}% for load and generation capacity, respectively"
f"Merged {len(i_islands_merge)} buses. Load attached to a single bus with discrepancies of {(100 * ((load_mean_final - load_mean_origin)/load_mean_origin)):2.1E}% and {(100 * ((generators_mean_final - generators_mean_origin)/generators_mean_origin)):2.1E}% for load and generation capacity, respectively"
)

return clustering.network, busmap
Expand Down Expand Up @@ -976,6 +1109,7 @@ def merge_isolated_nodes(n, threshold, aggregation_strategies=dict()):
0.0, cluster_config.get("p_threshold_drop_isolated", 0.0)
)
p_threshold_merge_isolated = cluster_config.get("p_threshold_merge_isolated", False)
s_threshold_fetch_isolated = cluster_config.get("s_threshold_fetch_isolated", False)

n = drop_isolated_nodes(n, threshold=p_threshold_drop_isolated)
if p_threshold_merge_isolated:
Expand All @@ -986,6 +1120,14 @@ def merge_isolated_nodes(n, threshold, aggregation_strategies=dict()):
)
busmaps.append(merged_nodes_map)

if s_threshold_fetch_isolated:
n, fetched_nodes_map = merge_into_network(
n,
threshold=s_threshold_fetch_isolated,
aggregation_strategies=aggregation_strategies,
)
busmaps.append(fetched_nodes_map)

n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards)))
n.export_to_netcdf(snakemake.output.network)

Expand Down
Loading