2018-01-29 21:28:33 +00:00
# coding: utf-8
2019-08-08 13:02:28 +00:00
"""
Create networks clustered to ` cluster ` number of zones with aggregated buses , generators and transmission corridors
2019-08-10 12:25:19 +00:00
2019-08-11 09:40:47 +00:00
Relevant Settings
- - - - - - - - - - - - - - - - -
Inputs
- - - - - -
Outputs
2019-08-10 12:25:19 +00:00
- - - - - - -
2019-08-11 09:40:47 +00:00
Description
- - - - - - - - - - -
2019-08-10 12:25:19 +00:00
The rule cluster_network instead clusters the network to a given number of buses .
- Why is this cluster function used ?
- Why the user can set a number behind the elec_sXXX for simplification ?
As you found out for yourself , elec_s100_50 . nc for example is a network in which simplify_network clusters the network to 100 buses and in a second step cluster_network reduces it down to 50 buses .
Well , let me provide a use - case where this makes sense :
In preliminary tests , it turns out , that the principal effect of changing spatial resolution is actually only partially due to the transmission network . It is more important to differentiate between wind generators with higher capacity factors from those with lower capacity factors , ie to have a higher spatial resolution in the renewable generation than in the number of buses .
This two - step clustering can take advantage of that fact ( and allows to study it )
by looking at networks like networks / elec_s100_50m . nc ( note the additional m in the cluster wildcard ) . For this example simplify_network clusters to 100 buses and then cluster_network clusters to 50 m buses , which means 50 buses for the network topology but only moving instead of aggregating the generators to the clustered buses . So in this network you still have up to 100 different wind generators , 2 at each bus on average .
In combination these two features allow you to study the spatial resolution of the transmission network separately from the spatial resolution of renewable generators . Beware : There is no clear evidence telling you what is a good representation of the full model . These options are under active study .
Why we have a cluster function inside of the simplification method ?
Why are you asking three times the same question ?
Is it possible to run the model without the simplification method / rule ?
I tryed to run the snakemake without the s for simplification .
No , the network clustering methods in PyPSA ' s networkclustering module don ' t work reliably with multiple voltage levels and transformers . If it is somehow necessary for you we could include switches to make Step 2 and 3 optional as well . But that ' s about it.
2019-08-08 13:02:28 +00:00
"""
2018-01-29 21:28:33 +00:00
import pandas as pd
idx = pd . IndexSlice
2018-01-30 22:11:16 +00:00
import logging
logger = logging . getLogger ( __name__ )
2018-01-29 21:28:33 +00:00
import os
import numpy as np
import scipy as sp
2018-01-30 22:11:16 +00:00
from scipy . sparse . csgraph import connected_components
2018-01-29 21:28:33 +00:00
import xarray as xr
import geopandas as gpd
import shapely
2018-01-30 22:11:16 +00:00
import networkx as nx
2018-03-13 09:59:44 +00:00
from shutil import copyfile
2018-01-29 21:28:33 +00:00
2018-01-30 22:11:16 +00:00
from six import iteritems
2018-01-29 21:28:33 +00:00
from six . moves import reduce
2018-02-19 09:03:25 +00:00
import pyomo . environ as po
2018-01-29 21:28:33 +00:00
import pypsa
2018-01-30 22:11:16 +00:00
from pypsa . io import import_components_from_dataframe , import_series_from_dataframe
2018-01-29 21:28:33 +00:00
from pypsa . networkclustering import ( busmap_by_stubs , busmap_by_kmeans ,
2018-01-30 22:11:16 +00:00
_make_consense , get_clustering_from_busmap ,
aggregategenerators , aggregateoneport )
2018-01-29 21:28:33 +00:00
def normed ( x ) :
return ( x / x . sum ( ) ) . fillna ( 0. )
2018-01-30 22:11:16 +00:00
def weighting_for_country ( n , x ) :
2019-02-18 17:36:56 +00:00
conv_carriers = { ' OCGT ' , ' CCGT ' , ' PHS ' , ' hydro ' }
2018-01-29 21:28:33 +00:00
gen = ( n
. generators . loc [ n . generators . carrier . isin ( conv_carriers ) ]
. groupby ( ' bus ' ) . p_nom . sum ( )
. reindex ( n . buses . index , fill_value = 0. ) +
n
. storage_units . loc [ n . storage_units . carrier . isin ( conv_carriers ) ]
. groupby ( ' bus ' ) . p_nom . sum ( )
. reindex ( n . buses . index , fill_value = 0. ) )
load = n . loads_t . p_set . mean ( ) . groupby ( n . loads . bus ) . sum ( )
b_i = x . index
g = normed ( gen . reindex ( b_i , fill_value = 0 ) )
l = normed ( load . reindex ( b_i , fill_value = 0 ) )
w = g + l
2018-08-27 17:14:34 +00:00
return ( w * ( 100. / w . max ( ) ) ) . clip ( lower = 1. ) . astype ( int )
2018-01-29 21:28:33 +00:00
## Plot weighting for Germany
2018-08-03 09:49:23 +00:00
def plot_weighting ( n , country , country_shape = None ) :
2018-01-29 21:28:33 +00:00
n . plot ( bus_sizes = ( 2 * weighting_for_country ( n . buses . loc [ n . buses . country == country ] ) ) . reindex ( n . buses . index , fill_value = 1 ) )
2018-08-03 09:49:23 +00:00
if country_shape is not None :
plt . xlim ( country_shape . bounds [ 0 ] , country_shape . bounds [ 2 ] )
plt . ylim ( country_shape . bounds [ 1 ] , country_shape . bounds [ 3 ] )
2018-01-29 21:28:33 +00:00
# # Determining the number of clusters per country
2019-01-22 10:47:28 +00:00
def distribute_clusters ( n , n_clusters , solver_name = None ) :
2018-09-14 09:22:13 +00:00
if solver_name is None :
solver_name = snakemake . config [ ' solver ' ] [ ' solver ' ] [ ' name ' ]
2018-02-19 09:03:25 +00:00
L = ( n . loads_t . p_set . mean ( )
. groupby ( n . loads . bus ) . sum ( )
. groupby ( [ n . buses . country , n . buses . sub_network ] ) . sum ( )
. pipe ( normed ) )
2019-01-22 10:25:01 +00:00
N = n . buses . groupby ( [ ' country ' , ' sub_network ' ] ) . size ( )
2019-02-13 18:03:57 +00:00
assert n_clusters > = len ( N ) and n_clusters < = N . sum ( ) , \
" Number of clusters must be {} <= n_clusters <= {} for this selection of countries. " . format ( len ( N ) , N . sum ( ) )
2018-02-19 09:03:25 +00:00
m = po . ConcreteModel ( )
2019-01-22 10:25:01 +00:00
def n_bounds ( model , * n_id ) :
return ( 1 , N [ n_id ] )
m . n = po . Var ( list ( L . index ) , bounds = n_bounds , domain = po . Integers )
2018-02-19 09:03:25 +00:00
m . tot = po . Constraint ( expr = ( po . summation ( m . n ) == n_clusters ) )
2019-03-09 16:50:28 +00:00
m . objective = po . Objective ( expr = sum ( ( m . n [ i ] - L . loc [ i ] * n_clusters ) * * 2 for i in L . index ) ,
2018-02-19 09:03:25 +00:00
sense = po . minimize )
opt = po . SolverFactory ( solver_name )
2019-02-10 13:43:57 +00:00
if not opt . has_capability ( ' quadratic_objective ' ) :
logger . warn ( f ' The configured solver ` { solver_name } ` does not support quadratic objectives. Falling back to `ipopt`. ' )
opt = po . SolverFactory ( ' ipopt ' )
2018-02-19 09:03:25 +00:00
results = opt . solve ( m )
assert results [ ' Solver ' ] [ 0 ] [ ' Status ' ] . key == ' ok ' , " Solver returned non-optimally: {} " . format ( results )
return pd . Series ( m . n . get_values ( ) , index = L . index ) . astype ( int )
2019-02-20 18:06:48 +00:00
def busmap_for_n_clusters ( n , n_clusters , solver_name , algorithm = " kmeans " , * * algorithm_kwds ) :
2019-02-06 11:06:32 +00:00
if algorithm == " kmeans " :
algorithm_kwds . setdefault ( ' n_init ' , 1000 )
algorithm_kwds . setdefault ( ' max_iter ' , 30000 )
algorithm_kwds . setdefault ( ' tol ' , 1e-6 )
2019-01-22 10:24:07 +00:00
2018-01-30 22:11:16 +00:00
n . determine_network_topology ( )
2019-01-22 10:47:28 +00:00
n_clusters = distribute_clusters ( n , n_clusters , solver_name = solver_name )
2018-02-19 09:03:25 +00:00
2019-02-06 11:06:32 +00:00
def reduce_network ( n , buses ) :
nr = pypsa . Network ( )
nr . import_components_from_dataframe ( buses , " Bus " )
nr . import_components_from_dataframe ( n . lines . loc [ n . lines . bus0 . isin ( buses . index ) & n . lines . bus1 . isin ( buses . index ) ] , " Line " )
return nr
2018-02-19 09:03:25 +00:00
2018-01-29 21:28:33 +00:00
def busmap_for_country ( x ) :
prefix = x . name [ 0 ] + x . name [ 1 ] + ' '
2019-01-22 10:24:07 +00:00
logger . debug ( " Determining busmap for country {} " . format ( prefix [ : - 1 ] ) )
2018-01-29 21:28:33 +00:00
if len ( x ) == 1 :
return pd . Series ( prefix + ' 0 ' , index = x . index )
2018-01-30 22:11:16 +00:00
weight = weighting_for_country ( n , x )
2019-02-06 11:06:32 +00:00
if algorithm == " kmeans " :
return prefix + busmap_by_kmeans ( n , weight , n_clusters [ x . name ] , buses_i = x . index , * * algorithm_kwds )
elif algorithm == " spectral " :
return prefix + busmap_by_spectral_clustering ( reduce_network ( n , x ) , n_clusters [ x . name ] , * * algorithm_kwds )
elif algorithm == " louvain " :
return prefix + busmap_by_louvain ( reduce_network ( n , x ) , n_clusters [ x . name ] , * * algorithm_kwds )
else :
raise ArgumentError ( " `algorithm` must be one of ' kmeans ' , ' spectral ' or ' louvain ' " )
2019-02-20 20:08:36 +00:00
return ( n . buses . groupby ( [ ' country ' , ' sub_network ' ] , group_keys = False , squeeze = True )
. apply ( busmap_for_country ) . rename ( ' busmap ' ) )
2018-01-29 21:28:33 +00:00
2018-01-30 22:11:16 +00:00
def plot_busmap_for_n_clusters ( n , n_clusters = 50 ) :
busmap = busmap_for_n_clusters ( n , n_clusters )
2018-01-29 21:28:33 +00:00
cs = busmap . unique ( )
cr = sns . color_palette ( " hls " , len ( cs ) )
n . plot ( bus_colors = busmap . map ( dict ( zip ( cs , cr ) ) ) )
del cs , cr
2018-12-31 13:53:11 +00:00
def clustering_for_n_clusters ( n , n_clusters , aggregate_carriers = None ,
2019-02-20 18:06:48 +00:00
line_length_factor = 1.25 , potential_mode = ' simple ' ,
solver_name = " cbc " , algorithm = " kmeans " ) :
2018-12-31 13:53:11 +00:00
2019-02-01 17:33:21 +00:00
if potential_mode == ' simple ' :
2018-12-31 13:53:11 +00:00
p_nom_max_strategy = np . sum
2019-02-01 17:33:21 +00:00
elif potential_mode == ' conservative ' :
p_nom_max_strategy = np . min
2018-12-31 13:53:11 +00:00
else :
2019-02-01 17:33:21 +00:00
raise AttributeError ( " potential_mode should be one of ' simple ' or ' conservative ' , "
2018-12-31 13:53:11 +00:00
" but is ' {} ' " . format ( potential_mode ) )
2018-01-29 21:28:33 +00:00
clustering = get_clustering_from_busmap (
2019-02-20 18:06:48 +00:00
n , busmap_for_n_clusters ( n , n_clusters , solver_name , algorithm ) ,
2018-01-29 21:28:33 +00:00
bus_strategies = dict ( country = _make_consense ( " Bus " , " country " ) ) ,
aggregate_generators_weighted = True ,
2018-12-31 13:53:11 +00:00
aggregate_generators_carriers = aggregate_carriers ,
2018-05-18 15:06:55 +00:00
aggregate_one_ports = [ " Load " , " StorageUnit " ] ,
2018-12-31 13:53:11 +00:00
line_length_factor = line_length_factor ,
generator_strategies = { ' p_nom_max ' : p_nom_max_strategy }
2018-01-29 21:28:33 +00:00
)
return clustering
def save_to_geojson ( s , fn ) :
if os . path . exists ( fn ) :
os . unlink ( fn )
2018-12-19 09:14:41 +00:00
df = s . reset_index ( )
schema = { * * gpd . io . file . infer_schema ( df ) , ' geometry ' : ' Unknown ' }
df . to_file ( fn , driver = ' GeoJSON ' , schema = schema )
2018-01-29 21:28:33 +00:00
2018-01-30 22:11:16 +00:00
def cluster_regions ( busmaps , input = None , output = None ) :
if input is None : input = snakemake . input
if output is None : output = snakemake . output
2018-01-29 21:28:33 +00:00
busmap = reduce ( lambda x , y : x . map ( y ) , busmaps [ 1 : ] , busmaps [ 0 ] )
for which in ( ' regions_onshore ' , ' regions_offshore ' ) :
2018-01-30 22:11:16 +00:00
regions = gpd . read_file ( getattr ( input , which ) ) . set_index ( ' name ' )
geom_c = regions . geometry . groupby ( busmap ) . apply ( shapely . ops . cascaded_union )
2018-01-29 21:28:33 +00:00
regions_c = gpd . GeoDataFrame ( dict ( geometry = geom_c ) )
2018-01-30 22:11:16 +00:00
regions_c . index . name = ' name '
save_to_geojson ( regions_c , getattr ( output , which ) )
2018-01-29 21:28:33 +00:00
if __name__ == " __main__ " :
# Detect running outside of snakemake and mock snakemake for testing
if ' snakemake ' not in globals ( ) :
2018-01-30 22:11:16 +00:00
from vresutils . snakemake import MockSnakemake , Dict
snakemake = MockSnakemake (
wildcards = Dict ( network = ' elec ' , simpl = ' ' , clusters = ' 45 ' ) ,
input = Dict (
network = ' networks/ {network} _s {simpl} .nc ' ,
regions_onshore = ' resources/regions_onshore_ {network} _s {simpl} .geojson ' ,
2019-02-20 20:08:36 +00:00
regions_offshore = ' resources/regions_offshore_ {network} _s {simpl} .geojson ' ,
clustermaps = ' resources/clustermaps_ {network} _s {simpl} .h5 '
2018-01-30 22:11:16 +00:00
) ,
output = Dict (
network = ' networks/ {network} _s {simpl} _ {clusters} .nc ' ,
regions_onshore = ' resources/regions_onshore_ {network} _s {simpl} _ {clusters} .geojson ' ,
2019-02-20 20:08:36 +00:00
regions_offshore = ' resources/regions_offshore_ {network} _s {simpl} _ {clusters} .geojson ' ,
clustermaps = ' resources/clustermaps_ {network} _s {simpl} _ {clusters} .h5 '
2018-01-30 22:11:16 +00:00
)
)
2018-02-10 16:16:20 +00:00
logging . basicConfig ( level = snakemake . config [ ' logging_level ' ] )
2018-01-29 21:28:33 +00:00
n = pypsa . Network ( snakemake . input . network )
2018-12-31 13:53:11 +00:00
renewable_carriers = pd . Index ( [ tech
for tech in n . generators . carrier . unique ( )
if tech . split ( ' - ' , 2 ) [ 0 ] in snakemake . config [ ' renewable ' ] ] )
2018-02-19 09:03:25 +00:00
if snakemake . wildcards . clusters . endswith ( ' m ' ) :
n_clusters = int ( snakemake . wildcards . clusters [ : - 1 ] )
2019-01-02 16:54:07 +00:00
aggregate_carriers = pd . Index ( n . generators . carrier . unique ( ) ) . difference ( renewable_carriers )
2018-02-19 09:03:25 +00:00
else :
n_clusters = int ( snakemake . wildcards . clusters )
2019-01-02 16:54:07 +00:00
aggregate_carriers = None # All
2018-02-19 09:03:25 +00:00
2018-03-13 09:59:44 +00:00
if n_clusters == len ( n . buses ) :
# Fast-path if no clustering is necessary
2018-03-13 10:50:06 +00:00
busmap = n . buses . index . to_series ( )
linemap = n . lines . index . to_series ( )
clustering = pypsa . networkclustering . Clustering ( n , busmap , linemap , linemap , pd . Series ( dtype = ' O ' ) )
2018-03-13 09:59:44 +00:00
else :
2018-05-18 15:06:55 +00:00
line_length_factor = snakemake . config [ ' lines ' ] [ ' length_factor ' ]
2018-12-31 13:53:11 +00:00
def consense ( x ) :
v = x . iat [ 0 ]
assert ( ( x == v ) . all ( ) or x . isnull ( ) . all ( ) ) , (
" The `potential` configuration option must agree for all renewable carriers, for now! "
)
return v
potential_mode = consense ( pd . Series ( [ snakemake . config [ ' renewable ' ] [ tech ] [ ' potential ' ]
for tech in renewable_carriers ] ) )
clustering = clustering_for_n_clusters ( n , n_clusters , aggregate_carriers ,
line_length_factor = line_length_factor ,
2019-02-20 18:06:48 +00:00
potential_mode = potential_mode ,
solver_name = snakemake . config [ ' solving ' ] [ ' solver ' ] [ ' name ' ] )
2018-01-29 21:28:33 +00:00
2018-03-13 10:50:06 +00:00
clustering . network . export_to_netcdf ( snakemake . output . network )
2018-09-27 10:18:47 +00:00
with pd . HDFStore ( snakemake . output . clustermaps , mode = ' w ' ) as store :
with pd . HDFStore ( snakemake . input . clustermaps , mode = ' r ' ) as clustermaps :
2018-07-10 14:31:57 +00:00
for attr in clustermaps . keys ( ) :
store . put ( attr , clustermaps [ attr ] , format = " table " , index = False )
2018-03-13 10:50:06 +00:00
for attr in ( ' busmap ' , ' linemap ' , ' linemap_positive ' , ' linemap_negative ' ) :
2018-03-14 12:19:44 +00:00
store . put ( attr , getattr ( clustering , attr ) , format = " table " , index = False )
2018-03-13 10:50:06 +00:00
cluster_regions ( ( clustering . busmap , ) )