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
Changes from 16 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
122 changes: 122 additions & 0 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 All @@ -104,6 +105,8 @@
)
from pypsa.io import import_components_from_dataframe, import_series_from_dataframe
from scipy.sparse.csgraph import connected_components, dijkstra
from scipy.spatial import cKDTree
from shapely.geometry import Point
Copy link
Member

Choose a reason for hiding this comment

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

Are they still needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed, thanks!


sys.settrace

Expand Down Expand Up @@ -738,6 +741,118 @@ def drop_isolated_nodes(n, threshold):
return n


def merge_into_network(n, aggregation_strategies=dict()):
"""
Find isolated nodes in the network and merge those of them which have load
value below than a specified threshold into a single isolated node which
represents all the remote generation.

Parameters
----------
n : PyPSA.Network
Original network
threshold : float
Load power used as a threshold to merge isolated nodes

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

n.determine_network_topology()

# duplicated sub-networks mean that there is at least one interconnection between buses
i_islands = n.buses[
(~n.buses.duplicated(subset=["sub_network"], keep=False))
& (n.buses.carrier == "AC")
].index
Copy link
Member

Choose a reason for hiding this comment

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

Does this account identifies isolated networks? This seems to me that nodes that are interconnected by a line but disconnected from the entire network are not well identified or am I wrong?

We could extend the working principle to "isolated networks" that may be defined as subnetworks that do not represent the larger national power system whose demand is low (below threshold).

To keep the approach simple we can also skip that

Copy link
Member Author

@ekatef ekatef Jan 4, 2024

Choose a reason for hiding this comment

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

Added a function to pick isolated networks using a number of the nodes as a criterium in the first approximation. Absolutely agree that it could make sense to use other heuristics, the current implementation just generalises the merging approach for n_isolated_buses > 1.


# TODO filtering may be applied to decide if the isolated buses should be fetched
# # 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[
# n.loads_t.p_set[i_load_islands].mean(axis=0) <= threshold
# ]

# return the original network if no isolated nodes are detected
if len(i_islands) == 0:
return n, n.buses.index.to_series()

i_connected = n.buses.loc[n.buses.carrier == "AC"].index.difference(i_islands)

points_buses = np.array(
list(zip(n.buses.loc[i_connected].x, n.buses.loc[i_connected].y))
)
islands_points = np.array(
list(zip(n.buses.loc[i_islands].x, n.buses.loc[i_islands].y))
)

gds_buses = gpd.GeoSeries(map(Point, points_buses))
gds_islands = gpd.GeoSeries(map(Point, islands_points))

gdf_buses = gpd.GeoDataFrame(geometry=gds_buses)
gdf_islands = gpd.GeoDataFrame(geometry=gds_islands)
Copy link
Member

Choose a reason for hiding this comment

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

to create a geodatafarme (let's say gdf_buses)of the entire buses df we may use this function, that should be quite compact

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree that it could be implemented in a more elegant way :) more Wrapped-up into a function transformation into a geopandas dataframe.

Have I understood your idea correctly? :)


gdf_map = gpd.sjoin_nearest(gdf_islands, gdf_buses, how="left")

nearest_bus_list = [
n.buses.loc[(n.buses.x == x) & (n.buses.y == y)]
for x, y in zip(gdf_map["geometry"].x, gdf_map["geometry"].y)
]
nearest_bus_df = pd.concat(nearest_bus_list)
Copy link
Member

Choose a reason for hiding this comment

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

I believe that nearest_bus_df = n.buses.loc[gdf_map.index_right.values] or something like that.

gdf_map.index_right/index_left may contain the mapping between each island and its closest bus.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, it feels much more natural to use indices for filtering. Added an bus_id column to the buses dataframe, and modified filtering.


# each isolated node should be mapped into the closes non-isolated node
map_isolated_node_by_country = (
n.buses.assign(bus_id=n.buses.index)
.loc[nearest_bus_df.index]
.groupby("country")["bus_id"]
.first()
.to_dict()
)
Copy link
Member

Choose a reason for hiding this comment

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

Something like:

gdf_buses.groupby(["country"{, "carrier"}]).map(lambda x: gdp.sjoin_nearest(x.query("is_isolated"), x.query("not is_isolated"), {options as needed}))

or

gdf_buses.groupby(["country"{, "carrier"}]).map(lambda x: gdp.sjoin_nearest(x.loc[i_islands], x.loc[i_connected], {options as needed}))

may work.

In the case of neighboring buses across countries I'm unsure the isolated network/node gets connected to the closest bus of the same country, it may remain there. Have you tested that?

isolated_buses_mapping = n.buses.loc[i_islands, "country"].replace(
map_isolated_node_by_country
)
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(islands_points)} 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 Down Expand Up @@ -981,6 +1096,13 @@ def merge_isolated_nodes(n, threshold, aggregation_strategies=dict()):
)
busmaps.append(merged_nodes_map)

# TODO Add a configuration option
n, fetched_nodes_map = merge_into_network(
n,
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