-
Notifications
You must be signed in to change notification settings - Fork 187
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
Changes from 16 commits
b992acd
f389d02
ea0eb88
a399ae1
e8262b8
52eb9a9
1af3d41
4aabc91
6391b21
bc8b755
a92a8fc
2ad6fa6
4d46fe5
e641447
febe2a7
dc9be87
06349a9
dd3a039
831182b
813ce0a
aa0f079
6743186
b79b364
4af66f0
b8d127e
2460f02
dc75850
1f7cc78
1430c66
1031926
085ee0c
3c9b651
4e45ecb
c103064
16ff175
bd71663
fef6935
db139ad
2bf9704
453583c
7ad0853
9efcdc6
92f9d69
222ab30
d1ef441
d4a212b
a0e9c87
71c398d
b167bf0
9e3466c
3656788
fd6354e
4046495
f8cb01f
b1697a0
fc97649
4ee2604
60ae7cd
b7f50c5
f2a7840
f00c04b
1702c3a
9b725cb
f1ec53b
6cf958b
3953b70
a3086f7
10cad77
171bd00
6f20a31
75019ac
ad8d083
278bc80
cc97b02
f92638e
a9b6f59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,6 +89,7 @@ | |
import sys | ||
from functools import reduce | ||
|
||
import geopandas as gpd | ||
import numpy as np | ||
import pandas as pd | ||
import pypsa | ||
|
@@ -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 | ||
|
||
sys.settrace | ||
|
||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to create a geodatafarme (let's say There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that gdf_map.index_right/index_left may contain the mapping between each island and its closest bus. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
# 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() | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something like:
or
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 | ||
|
@@ -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) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are they still needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, thanks!