Merge pull request #380 from PyPSA/harmonize_clustering_strategies

clustering strategies moved to configurables
This commit is contained in:
Martha Frysztacki 2022-06-27 14:35:05 +02:00 committed by GitHub
commit d3afd21377
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 112 additions and 55 deletions

View File

@ -22,6 +22,16 @@ countries: ['AL', 'AT', 'BA', 'BE', 'BG', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'F
clustering: clustering:
simplify: simplify:
to_substations: false # network is simplified to nodes with positive or negative power injection (i.e. substations or offwind connections) to_substations: false # network is simplified to nodes with positive or negative power injection (i.e. substations or offwind connections)
aggregation_strategies:
generators:
p_nom_max: sum # use "min" for more conservative assumptions
p_nom_min: sum
p_min_pu: mean
marginal_cost: mean
committable: any
ramp_limit_up: max
ramp_limit_down: max
efficiency: mean
snapshots: snapshots:
start: "2013-01-01" start: "2013-01-01"

View File

@ -22,6 +22,16 @@ countries: ['BE']
clustering: clustering:
simplify: simplify:
to_substations: false # network is simplified to nodes with positive or negative power injection (i.e. substations or offwind connections) to_substations: false # network is simplified to nodes with positive or negative power injection (i.e. substations or offwind connections)
aggregation_strategies:
generators:
p_nom_max: sum # use "min" for more conservative assumptions
p_nom_min: sum
p_min_pu: mean
marginal_cost: mean
committable: any
ramp_limit_up: max
ramp_limit_down: max
efficiency: mean
snapshots: snapshots:
start: "2013-03-01" start: "2013-03-01"

View File

@ -1,3 +1,8 @@
,Unit,Values,Description ,Unit,Values,Description
simplify,,, simplify,,,
-- to_substations,bool,"{'true','false'}","Aggregates all nodes without power injection (positive or negative, i.e. demand or generation) to electrically closest ones" -- to_substations,bool,"{'true','false'}","Aggregates all nodes without power injection (positive or negative, i.e. demand or generation) to electrically closest ones"
-- aggregation_strategies,,,
-- -- generators,,,
-- -- -- {key},str,"{key} can be any of the component of the generator (str). Its value can be any that can be converted to pandas.Series using getattr(). For example one of {min, max, sum}.","Aggregates the component according to the given strategy. For example, if sum, then all values within each cluster are summed to represent the new generator."
-- -- buses,,,
-- -- -- {key},str,"{key} can be any of the component of the bus (str). Its value can be any that can be converted to pandas.Series using getattr(). For example one of {min, max, sum}.","Aggregates the component according to the given strategy. For example, if sum, then all values within each cluster are summed to represent the new bus."

1 Unit Values Description
2 simplify
3 -- to_substations bool {'true','false'} Aggregates all nodes without power injection (positive or negative, i.e. demand or generation) to electrically closest ones
4 -- aggregation_strategies
5 -- -- generators
6 -- -- -- {key} str {key} can be any of the component of the generator (str). It’s value can be any that can be converted to pandas.Series using getattr(). For example one of {min, max, sum}. Aggregates the component according to the given strategy. For example, if sum, then all values within each cluster are summed to represent the new generator.
7 -- -- buses
8 -- -- -- {key} str {key} can be any of the component of the bus (str). It’s value can be any that can be converted to pandas.Series using getattr(). For example one of {min, max, sum}. Aggregates the component according to the given strategy. For example, if sum, then all values within each cluster are summed to represent the new bus.

View File

@ -79,6 +79,7 @@ Upcoming Release
* Update rasterio version to correctly calculate exclusion raster * Update rasterio version to correctly calculate exclusion raster
* Clustering strategies for generators and buses have moved from distinct scripts to configurables to unify the process and make it more transparent.
PyPSA-Eur 0.4.0 (22th September 2021) PyPSA-Eur 0.4.0 (22th September 2021)
===================================== =====================================

View File

@ -210,6 +210,22 @@ def progress_retrieve(url, file):
urllib.request.urlretrieve(url, file, reporthook=dlProgress) urllib.request.urlretrieve(url, file, reporthook=dlProgress)
def get_aggregation_strategies(aggregation_strategies):
# default aggregation strategies that cannot be defined in .yaml format must be specified within
# the function, otherwise (when defaults are passed in the function's definition) they get lost
# when custom values are specified in the config.
import numpy as np
from pypsa.networkclustering import _make_consense
bus_strategies = dict(country=_make_consense("Bus", "country"))
bus_strategies.update(aggregation_strategies.get("buses", {}))
generator_strategies = {'build_year': lambda x: 0, 'lifetime': lambda x: np.inf}
generator_strategies.update(aggregation_strategies.get("generators", {}))
return bus_strategies, generator_strategies
def mock_snakemake(rulename, **wildcards): def mock_snakemake(rulename, **wildcards):
""" """

View File

@ -11,11 +11,10 @@ Relevant Settings
.. code:: yaml .. code:: yaml
focus_weights: clustering:
aggregation_strategies:
renewable: (keys) focus_weights:
{technology}:
potential:
solving: solving:
solver: solver:
@ -122,7 +121,7 @@ Exemplary unsolved network clustered to 37 nodes:
""" """
import logging import logging
from _helpers import configure_logging, update_p_nom_max from _helpers import configure_logging, update_p_nom_max, get_aggregation_strategies
import pypsa import pypsa
import os import os
@ -259,15 +258,10 @@ def busmap_for_n_clusters(n, n_clusters, solver_name, focus_weights=None, algori
def clustering_for_n_clusters(n, n_clusters, custom_busmap=False, aggregate_carriers=None, def clustering_for_n_clusters(n, n_clusters, custom_busmap=False, aggregate_carriers=None,
line_length_factor=1.25, potential_mode='simple', solver_name="cbc", line_length_factor=1.25, aggregation_strategies=dict(), solver_name="cbc",
algorithm="kmeans", extended_link_costs=0, focus_weights=None): algorithm="kmeans", extended_link_costs=0, focus_weights=None):
if potential_mode == 'simple': bus_strategies, generator_strategies = get_aggregation_strategies(aggregation_strategies)
p_nom_max_strategy = pd.Series.sum
elif potential_mode == 'conservative':
p_nom_max_strategy = pd.Series.min
else:
raise AttributeError(f"potential_mode should be one of 'simple' or 'conservative' but is '{potential_mode}'")
if not isinstance(custom_busmap, pd.Series): if not isinstance(custom_busmap, pd.Series):
busmap = busmap_for_n_clusters(n, n_clusters, solver_name, focus_weights, algorithm) busmap = busmap_for_n_clusters(n, n_clusters, solver_name, focus_weights, algorithm)
@ -276,19 +270,12 @@ def clustering_for_n_clusters(n, n_clusters, custom_busmap=False, aggregate_carr
clustering = get_clustering_from_busmap( clustering = get_clustering_from_busmap(
n, busmap, n, busmap,
bus_strategies=dict(country=_make_consense("Bus", "country")), bus_strategies=bus_strategies,
aggregate_generators_weighted=True, aggregate_generators_weighted=True,
aggregate_generators_carriers=aggregate_carriers, aggregate_generators_carriers=aggregate_carriers,
aggregate_one_ports=["Load", "StorageUnit"], aggregate_one_ports=["Load", "StorageUnit"],
line_length_factor=line_length_factor, line_length_factor=line_length_factor,
generator_strategies={'p_nom_max': p_nom_max_strategy, generator_strategies=generator_strategies,
'p_nom_min': pd.Series.sum,
'p_min_pu': pd.Series.mean,
'marginal_cost': pd.Series.mean,
'committable': np.any,
'ramp_limit_up': pd.Series.max,
'ramp_limit_down': pd.Series.max,
},
scale_link_capital_costs=False) scale_link_capital_costs=False)
if not n.links.empty: if not n.links.empty:
@ -375,8 +362,13 @@ if __name__ == "__main__":
"The `potential` configuration option must agree for all renewable carriers, for now!" "The `potential` configuration option must agree for all renewable carriers, for now!"
) )
return v return v
potential_mode = consense(pd.Series([snakemake.config['renewable'][tech]['potential'] aggregation_strategies = snakemake.config["clustering"].get("aggregation_strategies", {})
for tech in renewable_carriers])) # translate str entries of aggregation_strategies to pd.Series functions:
aggregation_strategies = {
p: {k: getattr(pd.Series, v) for k,v in aggregation_strategies[p].items()}
for p in aggregation_strategies.keys()
}
custom_busmap = snakemake.config["enable"].get("custom_busmap", False) custom_busmap = snakemake.config["enable"].get("custom_busmap", False)
if custom_busmap: if custom_busmap:
custom_busmap = pd.read_csv(snakemake.input.custom_busmap, index_col=0, squeeze=True) custom_busmap = pd.read_csv(snakemake.input.custom_busmap, index_col=0, squeeze=True)
@ -384,12 +376,12 @@ if __name__ == "__main__":
logger.info(f"Imported custom busmap from {snakemake.input.custom_busmap}") logger.info(f"Imported custom busmap from {snakemake.input.custom_busmap}")
clustering = clustering_for_n_clusters(n, n_clusters, custom_busmap, aggregate_carriers, clustering = clustering_for_n_clusters(n, n_clusters, custom_busmap, aggregate_carriers,
line_length_factor, potential_mode, line_length_factor, aggregation_strategies,
snakemake.config['solving']['solver']['name'], snakemake.config['solving']['solver']['name'],
"kmeans", hvac_overhead_cost, focus_weights) "kmeans", hvac_overhead_cost, focus_weights)
update_p_nom_max(n) update_p_nom_max(clustering.network)
clustering.network.export_to_netcdf(snakemake.output.network) clustering.network.export_to_netcdf(snakemake.output.network)
for attr in ('busmap', 'linemap'): #also available: linemap_positive, linemap_negative for attr in ('busmap', 'linemap'): #also available: linemap_positive, linemap_negative
getattr(clustering, attr).to_csv(snakemake.output[attr]) getattr(clustering, attr).to_csv(snakemake.output[attr])

View File

@ -13,6 +13,10 @@ Relevant Settings
.. code:: yaml .. code:: yaml
clustering:
simplify:
aggregation_strategies:
costs: costs:
USD2013_to_EUR2013: USD2013_to_EUR2013:
discountrate: discountrate:
@ -22,10 +26,6 @@ Relevant Settings
electricity: electricity:
max_hours: max_hours:
renewables: (keys)
{technology}:
potential:
lines: lines:
length_factor: length_factor:
@ -83,7 +83,7 @@ The rule :mod:`simplify_network` does up to four things:
""" """
import logging import logging
from _helpers import configure_logging, update_p_nom_max from _helpers import configure_logging, update_p_nom_max, get_aggregation_strategies
from cluster_network import clustering_for_n_clusters, cluster_regions from cluster_network import clustering_for_n_clusters, cluster_regions
from add_electricity import load_costs from add_electricity import load_costs
@ -189,7 +189,10 @@ def _adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus, out
def _aggregate_and_move_components(n, busmap, connection_costs_to_bus, output, aggregate_one_ports={"Load", "StorageUnit"}): def _aggregate_and_move_components(n, busmap, connection_costs_to_bus, output,
aggregate_one_ports={"Load", "StorageUnit"},
aggregation_strategies=dict()):
def replace_components(n, c, df, pnl): def replace_components(n, c, df, pnl):
n.mremove(c, n.df(c).index) n.mremove(c, n.df(c).index)
@ -200,7 +203,11 @@ def _aggregate_and_move_components(n, busmap, connection_costs_to_bus, output, a
_adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus, output) _adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus, output)
generators, generators_pnl = aggregategenerators(n, busmap, custom_strategies={'p_nom_min': np.sum}) _, generator_strategies = get_aggregation_strategies(aggregation_strategies)
generators, generators_pnl = aggregategenerators(
n, busmap, custom_strategies=generator_strategies
)
replace_components(n, "Generator", generators, generators_pnl) replace_components(n, "Generator", generators, generators_pnl)
for one_port in aggregate_one_ports: for one_port in aggregate_one_ports:
@ -214,7 +221,7 @@ def _aggregate_and_move_components(n, busmap, connection_costs_to_bus, output, a
n.mremove(c, df.index[df.bus0.isin(buses_to_del) | df.bus1.isin(buses_to_del)]) n.mremove(c, df.index[df.bus0.isin(buses_to_del) | df.bus1.isin(buses_to_del)])
def simplify_links(n, costs, config, output): def simplify_links(n, costs, config, output, aggregation_strategies=dict()):
## Complex multi-node links are folded into end-points ## Complex multi-node links are folded into end-points
logger.info("Simplifying connected link components") logger.info("Simplifying connected link components")
@ -306,21 +313,23 @@ def simplify_links(n, costs, config, output):
logger.debug("Collecting all components using the busmap") logger.debug("Collecting all components using the busmap")
_aggregate_and_move_components(n, busmap, connection_costs_to_bus, output) _aggregate_and_move_components(n, busmap, connection_costs_to_bus, output,
aggregation_strategies=aggregation_strategies)
return n, busmap return n, busmap
def remove_stubs(n, costs, config, output): def remove_stubs(n, costs, config, output, aggregation_strategies=dict()):
logger.info("Removing stubs") logger.info("Removing stubs")
busmap = busmap_by_stubs(n) # ['country']) busmap = busmap_by_stubs(n) # ['country'])
connection_costs_to_bus = _compute_connection_costs_to_bus(n, busmap, costs, config) connection_costs_to_bus = _compute_connection_costs_to_bus(n, busmap, costs, config)
_aggregate_and_move_components(n, busmap, connection_costs_to_bus, output) _aggregate_and_move_components(n, busmap, connection_costs_to_bus, output,
aggregation_strategies=aggregation_strategies)
return n, busmap return n, busmap
def aggregate_to_substations(n, buses_i=None): def aggregate_to_substations(n, aggregation_strategies=dict(), buses_i=None):
# can be used to aggregate a selection of buses to electrically closest neighbors # can be used to aggregate a selection of buses to electrically closest neighbors
# if no buses are given, nodes that are no substations or without offshore connection are aggregated # if no buses are given, nodes that are no substations or without offshore connection are aggregated
@ -345,19 +354,21 @@ def aggregate_to_substations(n, buses_i=None):
busmap = n.buses.index.to_series() busmap = n.buses.index.to_series()
busmap.loc[buses_i] = dist.idxmin(1) busmap.loc[buses_i] = dist.idxmin(1)
bus_strategies, generator_strategies = get_aggregation_strategies(aggregation_strategies)
clustering = get_clustering_from_busmap(n, busmap, clustering = get_clustering_from_busmap(n, busmap,
bus_strategies=dict(country=_make_consense("Bus", "country")), bus_strategies=bus_strategies,
aggregate_generators_weighted=True, aggregate_generators_weighted=True,
aggregate_generators_carriers=None, aggregate_generators_carriers=None,
aggregate_one_ports=["Load", "StorageUnit"], aggregate_one_ports=["Load", "StorageUnit"],
line_length_factor=1.0, line_length_factor=1.0,
generator_strategies={'p_nom_max': 'sum'}, generator_strategies=generator_strategies,
scale_link_capital_costs=False) scale_link_capital_costs=False)
return clustering.network, busmap return clustering.network, busmap
def cluster(n, n_clusters, config): def cluster(n, n_clusters, config, aggregation_strategies=dict()):
logger.info(f"Clustering to {n_clusters} buses") logger.info(f"Clustering to {n_clusters} buses")
focus_weights = config.get('focus_weights', None) focus_weights = config.get('focus_weights', None)
@ -365,16 +376,9 @@ def cluster(n, n_clusters, config):
renewable_carriers = pd.Index([tech renewable_carriers = pd.Index([tech
for tech in n.generators.carrier.unique() for tech in n.generators.carrier.unique()
if tech.split('-', 2)[0] in config['renewable']]) if tech.split('-', 2)[0] in config['renewable']])
def consense(x):
v = x.iat[0] clustering = clustering_for_n_clusters(n, n_clusters, custom_busmap=False,
assert ((x == v).all() or x.isnull().all()), ( aggregation_strategies=aggregation_strategies,
"The `potential` configuration option must agree for all renewable carriers, for now!"
)
return v
potential_mode = (consense(pd.Series([config['renewable'][tech]['potential']
for tech in renewable_carriers]))
if len(renewable_carriers) > 0 else 'conservative')
clustering = clustering_for_n_clusters(n, n_clusters, custom_busmap=False, potential_mode=potential_mode,
solver_name=config['solving']['solver']['name'], solver_name=config['solving']['solver']['name'],
focus_weights=focus_weights) focus_weights=focus_weights)
@ -389,24 +393,33 @@ if __name__ == "__main__":
n = pypsa.Network(snakemake.input.network) n = pypsa.Network(snakemake.input.network)
aggregation_strategies = snakemake.config["clustering"].get("aggregation_strategies", {})
# translate str entries of aggregation_strategies to pd.Series functions:
aggregation_strategies = {
p: {k: getattr(pd.Series, v) for k,v in aggregation_strategies[p].items()}
for p in aggregation_strategies.keys()
}
n, trafo_map = simplify_network_to_380(n) n, trafo_map = simplify_network_to_380(n)
Nyears = n.snapshot_weightings.objective.sum() / 8760 Nyears = n.snapshot_weightings.objective.sum() / 8760
technology_costs = load_costs(snakemake.input.tech_costs, snakemake.config['costs'], snakemake.config['electricity'], Nyears) technology_costs = load_costs(snakemake.input.tech_costs, snakemake.config['costs'], snakemake.config['electricity'], Nyears)
n, simplify_links_map = simplify_links(n, technology_costs, snakemake.config, snakemake.output) n, simplify_links_map = simplify_links(n, technology_costs, snakemake.config, snakemake.output,
aggregation_strategies)
n, stub_map = remove_stubs(n, technology_costs, snakemake.config, snakemake.output) n, stub_map = remove_stubs(n, technology_costs, snakemake.config, snakemake.output,
aggregation_strategies=aggregation_strategies)
busmaps = [trafo_map, simplify_links_map, stub_map] busmaps = [trafo_map, simplify_links_map, stub_map]
if snakemake.config.get('clustering', {}).get('simplify', {}).get('to_substations', False): if snakemake.config.get('clustering', {}).get('simplify', {}).get('to_substations', False):
n, substation_map = aggregate_to_substations(n) n, substation_map = aggregate_to_substations(n, aggregation_strategies)
busmaps.append(substation_map) busmaps.append(substation_map)
if snakemake.wildcards.simpl: if snakemake.wildcards.simpl:
n, cluster_map = cluster(n, int(snakemake.wildcards.simpl), snakemake.config) n, cluster_map = cluster(n, int(snakemake.wildcards.simpl), snakemake.config, aggregation_strategies)
busmaps.append(cluster_map) busmaps.append(cluster_map)
# some entries in n.buses are not updated in previous functions, therefore can be wrong. as they are not needed # some entries in n.buses are not updated in previous functions, therefore can be wrong. as they are not needed

View File

@ -21,6 +21,16 @@ countries: ['BE']
clustering: clustering:
simplify: simplify:
to_substations: false # network is simplified to nodes with positive or negative power injection (i.e. substations or offwind connections) to_substations: false # network is simplified to nodes with positive or negative power injection (i.e. substations or offwind connections)
aggregation_strategies:
generators:
p_nom_max: sum # use "min" for more conservative assumptions
p_nom_min: sum
p_min_pu: mean
marginal_cost: mean
committable: any
ramp_limit_up: max
ramp_limit_down: max
efficiency: mean
snapshots: snapshots:
start: "2013-03-01" start: "2013-03-01"