2020-05-29 07:50:55 +00:00
# SPDX-FileCopyrightText: : 2017-2020 The PyPSA-Eur Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
2018-01-29 21:28:33 +00:00
# coding: utf-8
2019-08-08 13:02:28 +00:00
"""
2019-11-19 18:36:28 +00:00
Adds electrical generators and existing hydro storage units to a base network .
2019-08-11 09:40:47 +00:00
Relevant Settings
- - - - - - - - - - - - - - - - -
2019-08-11 11:17:36 +00:00
. . code : : yaml
costs :
year :
USD2013_to_EUR2013 :
dicountrate :
emission_prices :
electricity :
max_hours :
marginal_cost :
capital_cost :
conventional_carriers :
co2limit :
extendable_carriers :
2020-12-03 22:13:41 +00:00
include_renewable_capacities_from_OPSD :
2019-08-11 11:17:36 +00:00
estimate_renewable_capacities_from_capacity_stats :
load :
scaling_factor :
2020-12-05 16:54:50 +00:00
renewable :
2019-08-11 11:17:36 +00:00
hydro :
carriers :
hydro_max_hours :
hydro_capital_cost :
lines :
length_factor :
2019-10-30 22:09:41 +00:00
. . seealso : :
Documentation of the configuration file ` ` config . yaml ` ` at : ref : ` costs_cf ` ,
: ref : ` electricity_cf ` , : ref : ` load_cf ` , : ref : ` renewable_cf ` , : ref : ` lines_cf `
2019-08-13 08:03:46 +00:00
2019-08-11 09:40:47 +00:00
Inputs
- - - - - -
2019-08-12 17:01:53 +00:00
- ` ` data / costs . csv ` ` : The database of cost assumptions for all included technologies for specific years from various sources ; e . g . discount rate , lifetime , investment ( CAPEX ) , fixed operation and maintenance ( FOM ) , variable operation and maintenance ( VOM ) , fuel costs , efficiency , carbon - dioxide intensity .
- ` ` data / bundle / hydro_capacities . csv ` ` : Hydropower plant store / discharge power capacities , energy storage capacity , and average hourly inflow by country .
2019-08-14 13:36:46 +00:00
. . image : : . . / img / hydrocapacities . png
2019-08-12 17:01:53 +00:00
: scale : 34 %
2020-12-05 16:54:50 +00:00
- ` ` data / geth2015_hydro_capacities . csv ` ` : alternative to capacities above ; not currently used !
2020-12-03 11:49:04 +00:00
- ` ` resources / opsd_load . csv ` ` Hourly per - country load profiles .
2019-08-11 20:34:18 +00:00
- ` ` resources / regions_onshore . geojson ` ` : confer : ref : ` busregions `
- ` ` resources / nuts3_shapes . geojson ` ` : confer : ref : ` shapes `
- ` ` resources / powerplants . csv ` ` : confer : ref : ` powerplants `
- ` ` resources / profile_ { } . nc ` ` : all technologies in ` ` config [ " renewables " ] . keys ( ) ` ` , confer : ref : ` renewableprofiles ` .
- ` ` networks / base . nc ` ` : confer : ref : ` base `
2019-08-11 09:40:47 +00:00
Outputs
- - - - - - -
2019-08-11 20:34:18 +00:00
- ` ` networks / elec . nc ` ` :
2019-08-14 13:36:46 +00:00
. . image : : . . / img / elec . png
2019-08-11 20:34:18 +00:00
: scale : 33 %
2019-08-11 09:40:47 +00:00
Description
- - - - - - - - - - -
2019-08-13 15:52:33 +00:00
The rule : mod : ` add_electricity ` ties all the different data inputs from the preceding rules together into a detailed PyPSA network that is stored in ` ` networks / elec . nc ` ` . It includes :
2019-08-12 17:01:53 +00:00
2019-08-11 20:34:18 +00:00
- today ' s transmission topology and transfer capacities (optionally including lines which are under construction according to the config settings ``lines: under_construction`` and ``links: under_construction``),
- today ' s thermal and hydro power generation capacities (for the technologies listed in the config setting ``electricity: conventional_carriers``), and
- today ' s load time-series (upsampled in a top-down approach according to population and gross domestic product)
2019-11-19 18:36:28 +00:00
It further adds extendable ` ` generators ` ` with * * zero * * capacity for
2019-08-11 20:34:18 +00:00
2019-08-12 17:01:53 +00:00
- photovoltaic , onshore and AC - as well as DC - connected offshore wind installations with today ' s locational, hourly wind and solar capacity factors (but **no** current capacities),
2019-08-11 20:34:18 +00:00
- additional open - and combined - cycle gas turbines ( if ` ` OCGT ` ` and / or ` ` CCGT ` ` is listed in the config setting ` ` electricity : extendable_carriers ` ` )
2019-08-08 13:02:28 +00:00
"""
2018-01-29 21:28:33 +00:00
2019-11-14 16:50:24 +00:00
import logging
2019-11-28 07:22:52 +00:00
from _helpers import configure_logging
2020-12-03 18:50:53 +00:00
import pypsa
2018-01-29 21:28:33 +00:00
import pandas as pd
import numpy as np
import xarray as xr
import geopandas as gpd
2020-12-03 22:13:41 +00:00
import powerplantmatching as pm
from powerplantmatching . export import map_country_bus
2018-01-29 21:28:33 +00:00
2020-12-03 18:50:53 +00:00
from vresutils . costdata import annuity
from vresutils . load import timeseries_opsd
from vresutils import transfer as vtransfer
2019-11-14 16:50:24 +00:00
idx = pd . IndexSlice
2020-12-03 18:50:53 +00:00
logger = logging . getLogger ( __name__ )
2018-01-29 21:28:33 +00:00
def normed ( s ) : return s / s . sum ( )
2020-12-03 18:50:53 +00:00
2018-01-29 21:28:33 +00:00
def _add_missing_carriers_from_costs ( n , costs , carriers ) :
missing_carriers = pd . Index ( carriers ) . difference ( n . carriers . index )
2019-02-15 17:27:21 +00:00
if missing_carriers . empty : return
2019-10-30 22:09:41 +00:00
emissions_cols = costs . columns . to_series ( ) \
. loc [ lambda s : s . str . endswith ( ' _emissions ' ) ] . values
2019-02-15 17:27:21 +00:00
suptechs = missing_carriers . str . split ( ' - ' ) . str [ 0 ]
emissions = costs . loc [ suptechs , emissions_cols ] . fillna ( 0. )
emissions . index = missing_carriers
n . import_components_from_dataframe ( emissions , ' Carrier ' )
2018-01-29 21:28:33 +00:00
2020-12-03 18:50:53 +00:00
2018-05-18 15:09:18 +00:00
def load_costs ( Nyears = 1. , tech_costs = None , config = None , elec_config = None ) :
if tech_costs is None :
tech_costs = snakemake . input . tech_costs
if config is None :
config = snakemake . config [ ' costs ' ]
2018-01-29 21:28:33 +00:00
# set all asset costs and other parameters
2018-05-18 15:09:18 +00:00
costs = pd . read_csv ( tech_costs , index_col = list ( range ( 3 ) ) ) . sort_index ( )
2018-01-29 21:28:33 +00:00
# correct units to MW and EUR
costs . loc [ costs . unit . str . contains ( " /kW " ) , " value " ] * = 1e3
2018-05-18 15:09:18 +00:00
costs . loc [ costs . unit . str . contains ( " USD " ) , " value " ] * = config [ ' USD2013_to_EUR2013 ' ]
2018-01-29 21:28:33 +00:00
2019-10-30 22:09:41 +00:00
costs = ( costs . loc [ idx [ : , config [ ' year ' ] , : ] , " value " ]
. unstack ( level = 2 ) . groupby ( " technology " ) . sum ( min_count = 1 ) )
2018-01-29 21:28:33 +00:00
costs = costs . fillna ( { " CO2 intensity " : 0 ,
" FOM " : 0 ,
" VOM " : 0 ,
2018-05-18 15:09:18 +00:00
" discount rate " : config [ ' discountrate ' ] ,
2018-01-29 21:28:33 +00:00
" efficiency " : 1 ,
" fuel " : 0 ,
" investment " : 0 ,
" lifetime " : 25 } )
2019-10-30 22:09:41 +00:00
costs [ " capital_cost " ] = ( ( annuity ( costs [ " lifetime " ] , costs [ " discount rate " ] ) +
costs [ " FOM " ] / 100. ) *
2018-01-29 21:28:33 +00:00
costs [ " investment " ] * Nyears )
2018-07-25 15:15:06 +00:00
costs . at [ ' OCGT ' , ' fuel ' ] = costs . at [ ' gas ' , ' fuel ' ]
costs . at [ ' CCGT ' , ' fuel ' ] = costs . at [ ' gas ' , ' fuel ' ]
2018-01-29 21:28:33 +00:00
costs [ ' marginal_cost ' ] = costs [ ' VOM ' ] + costs [ ' fuel ' ] / costs [ ' efficiency ' ]
costs = costs . rename ( columns = { " CO2 intensity " : " co2_emissions " } )
2018-02-19 09:09:39 +00:00
costs . at [ ' OCGT ' , ' co2_emissions ' ] = costs . at [ ' gas ' , ' co2_emissions ' ]
2018-03-13 09:50:28 +00:00
costs . at [ ' CCGT ' , ' co2_emissions ' ] = costs . at [ ' gas ' , ' co2_emissions ' ]
2019-10-30 22:09:41 +00:00
costs . at [ ' solar ' , ' capital_cost ' ] = 0.5 * ( costs . at [ ' solar-rooftop ' , ' capital_cost ' ] +
costs . at [ ' solar-utility ' , ' capital_cost ' ] )
2018-02-19 09:09:39 +00:00
2018-01-29 21:28:33 +00:00
def costs_for_storage ( store , link1 , link2 = None , max_hours = 1. ) :
capital_cost = link1 [ ' capital_cost ' ] + max_hours * store [ ' capital_cost ' ]
if link2 is not None :
capital_cost + = link2 [ ' capital_cost ' ]
return pd . Series ( dict ( capital_cost = capital_cost ,
2018-01-30 22:12:36 +00:00
marginal_cost = 0. ,
2018-01-29 21:28:33 +00:00
co2_emissions = 0. ) )
2018-05-18 15:09:18 +00:00
if elec_config is None :
elec_config = snakemake . config [ ' electricity ' ]
max_hours = elec_config [ ' max_hours ' ]
2018-01-29 21:28:33 +00:00
costs . loc [ " battery " ] = \
costs_for_storage ( costs . loc [ " battery storage " ] , costs . loc [ " battery inverter " ] ,
max_hours = max_hours [ ' battery ' ] )
costs . loc [ " H2 " ] = \
2019-10-30 22:09:41 +00:00
costs_for_storage ( costs . loc [ " hydrogen storage " ] , costs . loc [ " fuel cell " ] ,
costs . loc [ " electrolysis " ] , max_hours = max_hours [ ' H2 ' ] )
2018-01-29 21:28:33 +00:00
for attr in ( ' marginal_cost ' , ' capital_cost ' ) :
2018-05-18 15:09:18 +00:00
overwrites = config . get ( attr )
2018-01-29 21:28:33 +00:00
if overwrites is not None :
overwrites = pd . Series ( overwrites )
costs . loc [ overwrites . index , attr ] = overwrites
return costs
2020-12-03 18:50:53 +00:00
2019-10-30 22:09:41 +00:00
def load_powerplants ( ppl_fn = None ) :
2018-02-10 16:16:20 +00:00
if ppl_fn is None :
ppl_fn = snakemake . input . powerplants
2020-12-03 18:50:53 +00:00
carrier_dict = { ' ocgt ' : ' OCGT ' , ' ccgt ' : ' CCGT ' , ' bioenergy ' : ' biomass ' ,
2019-10-30 22:09:41 +00:00
' ccgt, thermal ' : ' CCGT ' , ' hard coal ' : ' coal ' }
return ( pd . read_csv ( ppl_fn , index_col = 0 , dtype = { ' bus ' : ' str ' } )
. powerplant . to_pypsa_names ( )
. rename ( columns = str . lower ) . drop ( columns = [ ' efficiency ' ] )
. replace ( { ' carrier ' : carrier_dict } ) )
2018-01-29 21:28:33 +00:00
def attach_load ( n ) :
substation_lv_i = n . buses . index [ n . buses [ ' substation_lv ' ] ]
2019-10-30 22:09:41 +00:00
regions = ( gpd . read_file ( snakemake . input . regions ) . set_index ( ' name ' )
. reindex ( substation_lv_i ) )
2020-12-03 11:49:04 +00:00
opsd_load = ( pd . read_csv ( snakemake . input . load , index_col = 0 , parse_dates = True )
. filter ( items = snakemake . config [ ' countries ' ] ) )
2018-08-03 09:53:14 +00:00
2020-12-03 11:49:04 +00:00
scaling = snakemake . config . get ( ' load ' , { } ) . get ( ' scaling_factor ' , 1.0 )
logger . info ( f " Load data scaled with scalling factor { scaling } . " )
opsd_load * = scaling
2019-02-05 22:02:59 +00:00
2018-08-06 19:38:13 +00:00
nuts3 = gpd . read_file ( snakemake . input . nuts3_shapes ) . set_index ( ' index ' )
2018-08-03 09:53:14 +00:00
def upsample ( cntry , group ) :
l = opsd_load [ cntry ]
if len ( group ) == 1 :
return pd . DataFrame ( { group . index [ 0 ] : l } )
else :
nuts3_cntry = nuts3 . loc [ nuts3 . country == cntry ]
2019-10-30 22:09:41 +00:00
transfer = vtransfer . Shapes2Shapes ( group , nuts3_cntry . geometry ,
normed = False ) . T . tocsr ( )
gdp_n = pd . Series ( transfer . dot ( nuts3_cntry [ ' gdp ' ] . fillna ( 1. ) . values ) ,
index = group . index )
pop_n = pd . Series ( transfer . dot ( nuts3_cntry [ ' pop ' ] . fillna ( 1. ) . values ) ,
index = group . index )
2018-08-03 09:53:14 +00:00
# relative factors 0.6 and 0.4 have been determined from a linear
2020-12-03 11:49:04 +00:00
# regression on the country to continent load data
# (refer to vresutils.load._upsampling_weights)
2018-08-03 09:53:14 +00:00
factors = normed ( 0.6 * normed ( gdp_n ) + 0.4 * normed ( pop_n ) )
2019-10-30 22:09:41 +00:00
return pd . DataFrame ( factors . values * l . values [ : , np . newaxis ] ,
index = l . index , columns = factors . index )
2018-08-03 09:53:14 +00:00
2019-10-30 22:09:41 +00:00
load = pd . concat ( [ upsample ( cntry , group ) for cntry , group
in regions . geometry . groupby ( regions . country ) ] , axis = 1 )
2018-08-03 09:53:14 +00:00
n . madd ( " Load " , substation_lv_i , bus = substation_lv_i , p_set = load )
2018-01-29 21:28:33 +00:00
2018-10-22 21:22:34 +00:00
def update_transmission_costs ( n , costs , length_factor = 1.0 , simple_hvdc_costs = False ) :
2018-05-18 15:09:18 +00:00
n . lines [ ' capital_cost ' ] = ( n . lines [ ' length ' ] * length_factor *
2018-01-29 21:28:33 +00:00
costs . at [ ' HVAC overhead ' , ' capital_cost ' ] )
2019-02-13 18:03:57 +00:00
if n . links . empty : return
2018-01-29 21:28:33 +00:00
dc_b = n . links . carrier == ' DC '
2018-10-22 21:22:34 +00:00
if simple_hvdc_costs :
2019-10-30 22:09:41 +00:00
costs = ( n . links . loc [ dc_b , ' length ' ] * length_factor *
costs . at [ ' HVDC overhead ' , ' capital_cost ' ] )
2018-10-22 21:22:34 +00:00
else :
2019-10-30 22:09:41 +00:00
costs = ( n . links . loc [ dc_b , ' length ' ] * length_factor *
( ( 1. - n . links . loc [ dc_b , ' underwater_fraction ' ] ) *
costs . at [ ' HVDC overhead ' , ' capital_cost ' ] +
n . links . loc [ dc_b , ' underwater_fraction ' ] *
costs . at [ ' HVDC submarine ' , ' capital_cost ' ] ) +
costs . at [ ' HVDC inverter pair ' , ' capital_cost ' ] )
n . links . loc [ dc_b , ' capital_cost ' ] = costs
2019-11-19 18:36:28 +00:00
2018-01-29 21:28:33 +00:00
def attach_wind_and_solar ( n , costs ) :
for tech in snakemake . config [ ' renewable ' ] :
if tech == ' hydro ' : continue
n . add ( " Carrier " , name = tech )
with xr . open_dataset ( getattr ( snakemake . input , ' profile_ ' + tech ) ) as ds :
2019-02-22 16:09:52 +00:00
if ds . indexes [ ' bus ' ] . empty : continue
2018-12-19 09:30:25 +00:00
suptech = tech . split ( ' - ' , 2 ) [ 0 ]
if suptech == ' offwind ' :
underwater_fraction = ds [ ' underwater_fraction ' ] . to_pandas ( )
2019-10-30 22:09:41 +00:00
connection_cost = ( snakemake . config [ ' lines ' ] [ ' length_factor ' ] *
ds [ ' average_distance ' ] . to_pandas ( ) *
( underwater_fraction *
costs . at [ tech + ' -connection-submarine ' , ' capital_cost ' ] +
( 1. - underwater_fraction ) *
costs . at [ tech + ' -connection-underground ' , ' capital_cost ' ] ) )
capital_cost = ( costs . at [ ' offwind ' , ' capital_cost ' ] +
costs . at [ tech + ' -station ' , ' capital_cost ' ] +
connection_cost )
logger . info ( " Added connection cost of {:0.0f} - {:0.0f} Eur/MW/a to {} "
2019-10-31 12:01:08 +00:00
. format ( connection_cost . min ( ) , connection_cost . max ( ) , tech ) )
2018-12-19 09:30:25 +00:00
else :
capital_cost = costs . at [ tech , ' capital_cost ' ]
2018-01-29 21:28:33 +00:00
n . madd ( " Generator " , ds . indexes [ ' bus ' ] , ' ' + tech ,
bus = ds . indexes [ ' bus ' ] ,
carrier = tech ,
p_nom_extendable = True ,
p_nom_max = ds [ ' p_nom_max ' ] . to_pandas ( ) ,
weight = ds [ ' weight ' ] . to_pandas ( ) ,
2018-12-19 09:30:25 +00:00
marginal_cost = costs . at [ suptech , ' marginal_cost ' ] ,
2018-10-26 19:32:04 +00:00
capital_cost = capital_cost ,
2018-12-19 09:30:25 +00:00
efficiency = costs . at [ suptech , ' efficiency ' ] ,
2018-01-29 21:28:33 +00:00
p_max_pu = ds [ ' profile ' ] . transpose ( ' time ' , ' bus ' ) . to_pandas ( ) )
2018-07-10 14:29:11 +00:00
def attach_conventional_generators ( n , costs , ppl ) :
carriers = snakemake . config [ ' electricity ' ] [ ' conventional_carriers ' ]
2020-12-03 18:50:53 +00:00
2018-07-10 14:29:11 +00:00
_add_missing_carriers_from_costs ( n , costs , carriers )
2020-12-03 18:50:53 +00:00
2019-10-30 22:09:41 +00:00
ppl = ( ppl . query ( ' carrier in @carriers ' ) . join ( costs , on = ' carrier ' )
. rename ( index = lambda s : ' C ' + str ( s ) ) )
2018-07-10 14:29:11 +00:00
2020-12-03 22:13:41 +00:00
logger . info ( ' Adding {} generators with capacities [MW] \n {} '
2019-10-30 22:09:41 +00:00
. format ( len ( ppl ) , ppl . groupby ( ' carrier ' ) . p_nom . sum ( ) ) )
2020-12-03 18:50:53 +00:00
2019-10-30 22:09:41 +00:00
n . madd ( " Generator " , ppl . index ,
carrier = ppl . carrier ,
bus = ppl . bus ,
p_nom = ppl . p_nom ,
efficiency = ppl . efficiency ,
marginal_cost = ppl . marginal_cost ,
capital_cost = 0 )
2020-12-03 18:50:53 +00:00
2019-10-30 22:09:41 +00:00
logger . warning ( f ' Capital costs for conventional generators put to 0 EUR/MW. ' )
2018-01-29 21:28:33 +00:00
def attach_hydro ( n , costs , ppl ) :
2019-11-19 18:36:28 +00:00
if ' hydro ' not in snakemake . config [ ' renewable ' ] : return
2018-01-29 21:28:33 +00:00
c = snakemake . config [ ' renewable ' ] [ ' hydro ' ]
carriers = c . get ( ' carriers ' , [ ' ror ' , ' PHS ' , ' hydro ' ] )
_add_missing_carriers_from_costs ( n , costs , carriers )
2019-10-30 22:09:41 +00:00
ppl = ppl . query ( ' carrier == " hydro " ' ) . reset_index ( drop = True ) \
. rename ( index = lambda s : str ( s ) + ' hydro ' )
ror = ppl . query ( ' technology == " Run-Of-River " ' )
phs = ppl . query ( ' technology == " Pumped Storage " ' )
hydro = ppl . query ( ' technology == " Reservoir " ' )
2018-01-29 21:28:33 +00:00
2019-02-22 16:09:52 +00:00
country = ppl [ ' bus ' ] . map ( n . buses . country ) . rename ( " country " )
2019-11-01 12:27:42 +00:00
inflow_idx = ror . index | hydro . index
2019-10-30 22:09:41 +00:00
if not inflow_idx . empty :
dist_key = ppl . loc [ inflow_idx , ' p_nom ' ] . groupby ( country ) . transform ( normed )
2018-01-29 21:28:33 +00:00
2019-02-22 16:09:52 +00:00
with xr . open_dataarray ( snakemake . input . profile_hydro ) as inflow :
2019-10-30 22:09:41 +00:00
inflow_countries = pd . Index ( country [ inflow_idx ] )
missing_c = ( inflow_countries . unique ( )
. difference ( inflow . indexes [ ' countries ' ] ) )
assert missing_c . empty , ( f " ' { snakemake . input . profile_hydro } ' is missing "
f " inflow time-series for at least one country: { ' , ' . join ( missing_c ) } " )
inflow_t = ( inflow . sel ( countries = inflow_countries )
. rename ( { ' countries ' : ' name ' } )
. assign_coords ( name = inflow_idx )
. transpose ( ' time ' , ' name ' )
. to_pandas ( )
. multiply ( dist_key , axis = 1 ) )
if ' ror ' in carriers and not ror . empty :
n . madd ( " Generator " , ror . index ,
carrier = ' ror ' ,
bus = ror [ ' bus ' ] ,
p_nom = ror [ ' p_nom ' ] ,
efficiency = costs . at [ ' ror ' , ' efficiency ' ] ,
capital_cost = costs . at [ ' ror ' , ' capital_cost ' ] ,
weight = ror [ ' p_nom ' ] ,
p_max_pu = ( inflow_t [ ror . index ]
. divide ( ror [ ' p_nom ' ] , axis = 1 )
. where ( lambda df : df < = 1. , other = 1. ) ) )
if ' PHS ' in carriers and not phs . empty :
2020-12-03 18:50:53 +00:00
# fill missing max hours to config value and
# assume no natural inflow due to lack of data
2019-10-30 22:09:41 +00:00
phs = phs . replace ( { ' max_hours ' : { 0 : c [ ' PHS_max_hours ' ] } } )
n . madd ( ' StorageUnit ' , phs . index ,
carrier = ' PHS ' ,
bus = phs [ ' bus ' ] ,
p_nom = phs [ ' p_nom ' ] ,
capital_cost = costs . at [ ' PHS ' , ' capital_cost ' ] ,
max_hours = phs [ ' max_hours ' ] ,
efficiency_store = np . sqrt ( costs . at [ ' PHS ' , ' efficiency ' ] ) ,
efficiency_dispatch = np . sqrt ( costs . at [ ' PHS ' , ' efficiency ' ] ) ,
cyclic_state_of_charge = True )
if ' hydro ' in carriers and not hydro . empty :
hydro_max_hours = c . get ( ' hydro_max_hours ' )
2019-11-01 12:27:42 +00:00
hydro_stats = pd . read_csv ( snakemake . input . hydro_capacities ,
comment = " # " , na_values = ' - ' , index_col = 0 )
e_target = hydro_stats [ " E_store[TWh] " ] . clip ( lower = 0.2 ) * 1e6
e_installed = hydro . eval ( ' p_nom * max_hours ' ) . groupby ( hydro . country ) . sum ( )
e_missing = e_target - e_installed
missing_mh_i = hydro . query ( ' max_hours == 0 ' ) . index
2019-10-30 22:09:41 +00:00
if hydro_max_hours == ' energy_capacity_totals_by_country ' :
2019-11-01 12:27:42 +00:00
# watch out some p_nom values like IE's are totally underrepresented
max_hours_country = e_missing / \
hydro . loc [ missing_mh_i ] . groupby ( ' country ' ) . p_nom . sum ( )
2019-10-30 22:09:41 +00:00
elif hydro_max_hours == ' estimate_by_large_installations ' :
2019-11-01 12:27:42 +00:00
max_hours_country = hydro_stats [ ' E_store[TWh] ' ] * 1e3 / \
hydro_stats [ ' p_nom_discharge[GW] ' ]
missing_countries = ( pd . Index ( hydro [ ' country ' ] . unique ( ) )
. difference ( max_hours_country . dropna ( ) . index ) )
if not missing_countries . empty :
logger . warning ( " Assuming max_hours=6 for hydro reservoirs in the countries: {} "
. format ( " , " . join ( missing_countries ) ) )
hydro_max_hours = hydro . max_hours . where ( hydro . max_hours > 0 ,
hydro . country . map ( max_hours_country ) ) . fillna ( 6 )
2019-10-30 22:09:41 +00:00
n . madd ( ' StorageUnit ' , hydro . index , carrier = ' hydro ' ,
bus = hydro [ ' bus ' ] ,
p_nom = hydro [ ' p_nom ' ] ,
max_hours = hydro_max_hours ,
capital_cost = ( costs . at [ ' hydro ' , ' capital_cost ' ]
if c . get ( ' hydro_capital_cost ' ) else 0. ) ,
marginal_cost = costs . at [ ' hydro ' , ' marginal_cost ' ] ,
p_max_pu = 1. , # dispatch
p_min_pu = 0. , # store
efficiency_dispatch = costs . at [ ' hydro ' , ' efficiency ' ] ,
efficiency_store = 0. ,
cyclic_state_of_charge = True ,
inflow = inflow_t . loc [ : , hydro . index ] )
2018-01-29 21:28:33 +00:00
def attach_extendable_generators ( n , costs , ppl ) :
elec_opts = snakemake . config [ ' electricity ' ]
2019-02-15 17:27:21 +00:00
carriers = pd . Index ( elec_opts [ ' extendable_carriers ' ] [ ' Generator ' ] )
2020-12-03 18:50:53 +00:00
2018-01-29 21:28:33 +00:00
_add_missing_carriers_from_costs ( n , costs , carriers )
2019-02-15 17:27:21 +00:00
for tech in carriers :
2019-11-19 18:36:28 +00:00
if tech . startswith ( ' OCGT ' ) :
2019-10-30 22:09:41 +00:00
ocgt = ppl . query ( " carrier in [ ' OCGT ' , ' CCGT ' ] " ) . groupby ( ' bus ' , as_index = False ) . first ( )
2019-02-15 17:27:21 +00:00
n . madd ( ' Generator ' , ocgt . index ,
2019-02-17 23:43:09 +00:00
suffix = ' OCGT ' ,
2019-02-15 17:27:21 +00:00
bus = ocgt [ ' bus ' ] ,
carrier = tech ,
p_nom_extendable = True ,
p_nom = 0. ,
capital_cost = costs . at [ ' OCGT ' , ' capital_cost ' ] ,
marginal_cost = costs . at [ ' OCGT ' , ' marginal_cost ' ] ,
efficiency = costs . at [ ' OCGT ' , ' efficiency ' ] )
2019-02-16 09:41:21 +00:00
2019-11-19 18:36:28 +00:00
elif tech . startswith ( ' CCGT ' ) :
2019-10-30 22:09:41 +00:00
ccgt = ppl . query ( " carrier in [ ' OCGT ' , ' CCGT ' ] " ) . groupby ( ' bus ' , as_index = False ) . first ( )
2019-02-16 09:41:21 +00:00
n . madd ( ' Generator ' , ccgt . index ,
2019-02-17 23:43:09 +00:00
suffix = ' CCGT ' ,
2019-02-16 09:41:21 +00:00
bus = ccgt [ ' bus ' ] ,
carrier = tech ,
p_nom_extendable = True ,
p_nom = 0. ,
capital_cost = costs . at [ ' CCGT ' , ' capital_cost ' ] ,
marginal_cost = costs . at [ ' CCGT ' , ' marginal_cost ' ] ,
efficiency = costs . at [ ' CCGT ' , ' efficiency ' ] )
2019-11-19 11:26:01 +00:00
2019-11-19 18:36:28 +00:00
elif tech . startswith ( ' nuclear ' ) :
2019-11-19 11:26:01 +00:00
nuclear = ppl . query ( " carrier == ' nuclear ' " ) . groupby ( ' bus ' , as_index = False ) . first ( )
n . madd ( ' Generator ' , nuclear . index ,
suffix = ' nuclear ' ,
bus = nuclear [ ' bus ' ] ,
carrier = tech ,
p_nom_extendable = True ,
p_nom = 0. ,
capital_cost = costs . at [ ' nuclear ' , ' capital_cost ' ] ,
marginal_cost = costs . at [ ' nuclear ' , ' marginal_cost ' ] ,
efficiency = costs . at [ ' nuclear ' , ' efficiency ' ] )
2019-02-15 17:27:21 +00:00
else :
2019-10-30 22:09:41 +00:00
raise NotImplementedError ( f " Adding extendable generators for carrier "
" ' {tech} ' is not implemented, yet. "
2019-11-19 11:26:01 +00:00
" Only OCGT, CCGT and nuclear are allowed at the moment. " )
2018-01-29 21:28:33 +00:00
2020-12-03 22:13:41 +00:00
def attach_OPSD_renewables ( n ) :
available = [ ' DE ' , ' FR ' , ' PL ' , ' CH ' , ' DK ' , ' CZ ' , ' SE ' , ' GB ' ]
tech_map = { ' Onshore ' : ' onwind ' , ' Offshore ' : ' offwind ' , ' Solar ' : ' solar ' }
countries = set ( available ) & set ( n . buses . country )
techs = snakemake . config [ ' electricity ' ] . get ( ' renewable_capacities_from_OPSD ' , [ ] )
tech_map = { k : v for k , v in tech_map . items ( ) if v in techs }
if not tech_map :
return
logger . info ( f ' Using OPSD renewable capacities in { " , " . join ( countries ) } '
f ' for technologies { " , " . join ( tech_map . values ( ) ) } . ' )
df = pd . concat ( [ pm . data . OPSD_VRE_country ( c ) for c in countries ] )
technology_b = ~ df . Technology . isin ( [ ' Onshore ' , ' Offshore ' ] )
df [ ' Fueltype ' ] = df . Fueltype . where ( technology_b , df . Technology )
df = df . query ( ' Fueltype in @tech_map ' ) . powerplant . convert_country_to_alpha2 ( )
for fueltype , carrier_like in tech_map . items ( ) :
gens = n . generators [ lambda df : df . carrier . str . contains ( carrier_like ) ]
buses = n . buses . loc [ gens . bus . unique ( ) ]
gens_per_bus = gens . groupby ( ' bus ' ) . p_nom . count ( )
caps = map_country_bus ( df . query ( ' Fueltype == @fueltype ' ) , buses )
caps = caps . groupby ( [ ' bus ' ] ) . Capacity . sum ( )
caps = caps / gens_per_bus . reindex ( caps . index , fill_value = 1 )
n . generators . p_nom . update ( gens . bus . map ( caps ) . dropna ( ) )
2019-02-05 22:00:35 +00:00
def estimate_renewable_capacities ( n , tech_map = None ) :
if tech_map is None :
2019-10-30 22:09:41 +00:00
tech_map = ( snakemake . config [ ' electricity ' ]
. get ( ' estimate_renewable_capacities_from_capacity_stats ' , { } ) )
2019-02-05 22:00:35 +00:00
if len ( tech_map ) == 0 : return
2020-12-03 22:13:41 +00:00
capacities = ( pm . data . Capacity_stats ( ) . powerplant . convert_country_to_alpha2 ( )
2019-10-30 22:09:41 +00:00
[ lambda df : df . Energy_Source_Level_2 ]
. set_index ( [ ' Fueltype ' , ' Country ' ] ) . sort_index ( ) )
2019-02-05 22:00:35 +00:00
countries = n . buses . country . unique ( )
2020-12-03 22:13:41 +00:00
if len ( countries ) == 0 : return
logger . info ( ' heuristics applied to distribute renewable capacities [MW] \n {} '
. format ( capacities . query ( ' Fueltype in @tech_map.keys() and Capacity >= 0.1 ' )
. groupby ( ' Country ' ) . agg ( { ' Capacity ' : ' sum ' } ) ) )
2019-02-05 22:00:35 +00:00
for ppm_fueltype , techs in tech_map . items ( ) :
2019-10-30 22:09:41 +00:00
tech_capacities = capacities . loc [ ppm_fueltype , ' Capacity ' ] \
. reindex ( countries , fill_value = 0. )
2020-12-03 22:13:41 +00:00
#tech_i = n.generators.query('carrier in @techs').index
tech_i = ( n . generators . query ( ' carrier in @techs ' )
[ n . generators . query ( ' carrier in @techs ' )
. bus . map ( n . buses . country ) . isin ( countries ) ] . index )
2019-10-30 22:09:41 +00:00
n . generators . loc [ tech_i , ' p_nom ' ] = (
( n . generators_t . p_max_pu [ tech_i ] . mean ( ) *
n . generators . loc [ tech_i , ' p_nom_max ' ] ) # maximal yearly generation
2020-12-03 18:50:53 +00:00
. groupby ( n . generators . bus . map ( n . buses . country ) )
2019-10-30 22:09:41 +00:00
. transform ( lambda s : normed ( s ) * tech_capacities . at [ s . name ] )
. where ( lambda s : s > 0.1 , 0. ) ) # only capacities above 100kW
2018-01-29 21:28:33 +00:00
2020-12-03 18:50:53 +00:00
2019-11-19 18:36:28 +00:00
def add_nice_carrier_names ( n , config = None ) :
if config is None : config = snakemake . config
2020-08-24 09:04:54 +00:00
carrier_i = n . carriers . index
nice_names = ( pd . Series ( config [ ' plotting ' ] [ ' nice_names ' ] )
. reindex ( carrier_i ) . fillna ( carrier_i . to_series ( ) . str . title ( ) ) )
n . carriers [ ' nice_name ' ] = nice_names
colors = pd . Series ( config [ ' plotting ' ] [ ' tech_colors ' ] ) . reindex ( carrier_i )
if colors . isna ( ) . any ( ) :
missing_i = list ( colors . index [ colors . isna ( ) ] )
logger . warning ( f ' tech_colors for carriers { missing_i } not defined '
' in config. ' )
n . carriers [ ' color ' ] = colors
2019-11-09 08:08:25 +00:00
2018-01-29 21:28:33 +00:00
if __name__ == " __main__ " :
if ' snakemake ' not in globals ( ) :
2019-12-09 20:29:15 +00:00
from _helpers import mock_snakemake
snakemake = mock_snakemake ( ' add_electricity ' )
2019-11-28 07:22:52 +00:00
configure_logging ( snakemake )
2018-01-29 21:28:33 +00:00
n = pypsa . Network ( snakemake . input . base_network )
2020-12-03 18:50:53 +00:00
Nyears = n . snapshot_weightings . sum ( ) / 8760.
2018-01-29 21:28:33 +00:00
costs = load_costs ( Nyears )
2019-10-30 22:09:41 +00:00
ppl = load_powerplants ( )
2018-01-29 21:28:33 +00:00
attach_load ( n )
update_transmission_costs ( n , costs )
2019-11-19 18:36:28 +00:00
attach_conventional_generators ( n , costs , ppl )
2018-01-29 21:28:33 +00:00
attach_wind_and_solar ( n , costs )
2019-11-19 18:36:28 +00:00
attach_hydro ( n , costs , ppl )
2018-01-29 21:28:33 +00:00
attach_extendable_generators ( n , costs , ppl )
2019-02-05 22:00:35 +00:00
estimate_renewable_capacities ( n )
2020-12-03 22:13:41 +00:00
attach_OPSD_renewables ( n )
2019-11-09 08:08:25 +00:00
add_nice_carrier_names ( n )
2019-02-05 22:00:35 +00:00
2018-01-29 21:28:33 +00:00
n . export_to_netcdf ( snakemake . output [ 0 ] )