2022-09-16 13:04:04 +00:00
# -*- coding: utf-8 -*-
2023-02-16 10:50:55 +00:00
# SPDX-FileCopyrightText: : 2017-2023 The PyPSA-Eur Authors
2020-05-29 07:50:55 +00:00
#
2021-09-14 14:37:41 +00:00
# SPDX-License-Identifier: MIT
2020-05-29 07:50:55 +00:00
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 :
2020-10-16 09:38:26 +00:00
version :
2019-08-11 11:17:36 +00:00
dicountrate :
emission_prices :
electricity :
max_hours :
marginal_cost :
capital_cost :
conventional_carriers :
co2limit :
extendable_carriers :
2022-06-09 18:31:50 +00:00
estimate_renewable_capacities :
2022-06-09 21:40:32 +00:00
2019-08-11 11:17:36 +00:00
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 : :
2023-04-21 08:41:44 +00:00
Documentation of the configuration file ` ` config / config . yaml ` ` at : ref : ` costs_cf ` ,
2019-10-30 22:09:41 +00:00
: 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
- - - - - -
2020-10-16 09:38:26 +00:00
- ` ` resources / 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 .
2019-08-12 17:01:53 +00:00
- ` ` data / bundle / hydro_capacities . csv ` ` : Hydropower plant store / discharge power capacities , energy storage capacity , and average hourly inflow by country .
2023-03-09 12:28:42 +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 !
2022-07-04 16:42:22 +00:00
- ` ` resources / 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 ` ` :
2023-03-09 12:28:42 +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
2023-05-10 08:09:43 +00:00
from itertools import product
2019-11-28 07:22:52 +00:00
2018-01-29 21:28:33 +00:00
import geopandas as gpd
2022-09-16 13:04:04 +00:00
import numpy as np
import pandas as pd
2020-12-03 22:13:41 +00:00
import powerplantmatching as pm
2022-09-16 13:04:04 +00:00
import pypsa
2023-05-10 08:09:43 +00:00
import scipy . sparse as sparse
2022-09-16 13:04:04 +00:00
import xarray as xr
from _helpers import configure_logging , update_p_nom_max
2020-12-03 22:13:41 +00:00
from powerplantmatching . export import map_country_bus
2023-05-10 07:58:25 +00:00
from shapely . prepared import prep
2020-12-03 18:50:53 +00:00
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
2022-09-16 13:04:04 +00:00
def normed ( s ) :
return s / s . sum ( )
2018-01-29 21:28:33 +00:00
2020-12-03 18:50:53 +00:00
2022-04-15 16:36:37 +00:00
def calculate_annuity ( n , r ) :
2022-09-16 13:04:04 +00:00
"""
Calculate the annuity factor for an asset with lifetime n years and .
discount rate of r , e . g . annuity ( 20 , 0.05 ) * 20 = 1.6
"""
2022-04-15 16:36:37 +00:00
if isinstance ( r , pd . Series ) :
2022-09-16 13:04:04 +00:00
return pd . Series ( 1 / n , index = r . index ) . where (
r == 0 , r / ( 1.0 - 1.0 / ( 1.0 + r ) * * n )
)
2022-04-15 16:36:37 +00:00
elif r > 0 :
2022-09-16 13:04:04 +00:00
return r / ( 1.0 - 1.0 / ( 1.0 + r ) * * n )
2022-04-15 16:36:37 +00:00
else :
return 1 / n
2023-06-30 11:28:54 +00:00
2023-06-30 11:25:20 +00:00
def add_missing_carriers ( n , carriers ) :
"""
Function to add missing carriers to the network without raising errors .
"""
missing_carriers = set ( carriers ) - set ( n . carriers . index )
if len ( missing_carriers ) > 0 :
n . madd ( " Carrier " , missing_carriers )
2023-04-25 15:26:23 +00:00
2023-06-30 11:28:54 +00:00
2023-05-03 11:24:57 +00:00
def sanitize_carriers ( n , config ) :
"""
Sanitize the carrier information in a PyPSA Network object .
The function ensures that all unique carrier names are present in the network ' s
carriers attribute , and adds nice names and colors for each carrier according
to the provided configuration dictionary .
Parameters
- - - - - - - - - -
n : pypsa . Network
A PyPSA Network object that represents an electrical power system .
config : dict
A dictionary containing configuration information , specifically the
" plotting " key with " nice_names " and " tech_colors " keys for carriers .
Returns
- - - - - - -
None
The function modifies the ' n ' PyPSA Network object in - place , updating the
carriers attribute with nice names and colors .
Warnings
- - - - - - - -
Raises a warning if any carrier ' s " tech_colors " are not defined in the config dictionary.
"""
for c in n . iterate_components ( ) :
if " carrier " in c . df :
2023-07-06 13:01:00 +00:00
add_missing_carriers ( n , c . df . carrier )
2023-04-25 15:26:23 +00:00
2023-04-28 01:43:20 +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 ( ) )
)
2023-06-29 14:02:54 +00:00
n . carriers [ " nice_name " ] = n . carriers . nice_name . where (
n . carriers . nice_name != " " , nice_names
)
2023-04-28 01:43:20 +00:00
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. " )
2023-06-29 14:02:09 +00:00
n . carriers [ " color " ] = n . carriers . color . where ( n . carriers . color != " " , colors )
2023-04-28 01:43:20 +00:00
2022-04-15 16:36:37 +00:00
2023-05-03 11:24:57 +00:00
def add_co2_emissions ( n , costs , carriers ) :
"""
Add CO2 emissions to the network ' s carriers attribute.
"""
suptechs = n . carriers . loc [ carriers ] . index . str . split ( " - " ) . str [ 0 ]
n . carriers . loc [ carriers , " co2_emissions " ] = costs . co2_emissions [ suptechs ] . values
2020-12-03 18:50:53 +00:00
2018-01-29 21:28:33 +00:00
2023-06-15 17:35:41 +00:00
def load_costs ( tech_costs , config , max_hours , Nyears = 1.0 ) :
2018-01-29 21:28:33 +00:00
# set all asset costs and other parameters
2022-09-16 13:04:04 +00:00
costs = pd . read_csv ( tech_costs , index_col = [ 0 , 1 ] ) . sort_index ( )
2018-01-29 21:28:33 +00:00
2020-08-25 19:41:21 +00:00
# correct units to MW
2022-09-16 13:04:04 +00:00
costs . loc [ costs . unit . str . contains ( " /kW " ) , " value " ] * = 1e3
2020-10-16 09:38:26 +00:00
costs . unit = costs . unit . str . replace ( " /kW " , " /MW " )
2018-01-29 21:28:33 +00:00
2020-10-16 09:38:26 +00:00
fill_values = config [ " fill_values " ]
2020-08-25 19:41:21 +00:00
costs = costs . value . unstack ( ) . fillna ( fill_values )
2018-01-29 21:28:33 +00:00
2022-09-16 13:04:04 +00:00
costs [ " capital_cost " ] = (
(
calculate_annuity ( costs [ " lifetime " ] , costs [ " discount rate " ] )
+ costs [ " FOM " ] / 100.0
)
* costs [ " investment " ]
* Nyears
)
2018-07-25 15:15:06 +00:00
2022-09-16 13:04:04 +00:00
costs . at [ " OCGT " , " fuel " ] = costs . at [ " gas " , " fuel " ]
costs . at [ " CCGT " , " fuel " ] = costs . at [ " gas " , " fuel " ]
2018-07-25 15:15:06 +00:00
2022-09-16 13:04:04 +00:00
costs [ " marginal_cost " ] = costs [ " VOM " ] + costs [ " fuel " ] / costs [ " efficiency " ]
2018-01-29 21:28:33 +00:00
costs = costs . rename ( columns = { " CO2 intensity " : " co2_emissions " } )
2022-09-16 13:04:04 +00:00
costs . at [ " OCGT " , " co2_emissions " ] = costs . at [ " gas " , " co2_emissions " ]
costs . at [ " CCGT " , " co2_emissions " ] = costs . at [ " gas " , " co2_emissions " ]
2018-03-13 09:50:28 +00:00
2022-09-16 13:04:04 +00:00
costs . at [ " solar " , " capital_cost " ] = (
config [ " rooftop_share " ] * costs . at [ " solar-rooftop " , " capital_cost " ]
+ ( 1 - config [ " rooftop_share " ] ) * costs . at [ " solar-utility " , " capital_cost " ]
)
2018-02-19 09:09:39 +00:00
2022-09-16 13:04:04 +00:00
def costs_for_storage ( store , link1 , link2 = None , max_hours = 1.0 ) :
capital_cost = link1 [ " capital_cost " ] + max_hours * store [ " capital_cost " ]
2018-01-29 21:28:33 +00:00
if link2 is not None :
2022-09-16 13:04:04 +00:00
capital_cost + = link2 [ " capital_cost " ]
return pd . Series (
dict ( capital_cost = capital_cost , marginal_cost = 0.0 , co2_emissions = 0.0 )
)
costs . loc [ " battery " ] = costs_for_storage (
costs . loc [ " battery storage " ] ,
costs . loc [ " battery inverter " ] ,
max_hours = max_hours [ " battery " ] ,
)
costs . loc [ " H2 " ] = costs_for_storage (
costs . loc [ " hydrogen storage underground " ] ,
costs . loc [ " fuel cell " ] ,
costs . loc [ " electrolysis " ] ,
max_hours = max_hours [ " H2 " ] ,
)
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
2021-09-14 14:34:02 +00:00
def load_powerplants ( ppl_fn ) :
2022-09-16 13:04:04 +00:00
carrier_dict = {
" ocgt " : " OCGT " ,
" ccgt " : " CCGT " ,
" bioenergy " : " biomass " ,
" 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 )
. replace ( { " carrier " : carrier_dict } )
)
2023-05-11 14:58:35 +00:00
def shapes_to_shapes ( orig , dest ) :
2023-05-10 07:58:25 +00:00
"""
Adopted from vresutils . transfer . Shapes2Shapes ( )
"""
orig_prepped = list ( map ( prep , orig ) )
transfer = sparse . lil_matrix ( ( len ( dest ) , len ( orig ) ) , dtype = float )
2023-05-10 08:09:43 +00:00
for i , j in product ( range ( len ( dest ) ) , range ( len ( orig ) ) ) :
2023-05-10 07:58:25 +00:00
if orig_prepped [ j ] . intersects ( dest [ i ] ) :
area = orig [ j ] . intersection ( dest [ i ] ) . area
2023-05-10 08:09:43 +00:00
transfer [ i , j ] = area / dest [ i ] . area
2023-05-10 07:58:25 +00:00
return transfer
2022-09-16 13:04:04 +00:00
def attach_load ( n , regions , load , nuts3_shapes , countries , scaling = 1.0 ) :
substation_lv_i = n . buses . index [ n . buses [ " substation_lv " ] ]
regions = gpd . read_file ( regions ) . set_index ( " name " ) . reindex ( substation_lv_i )
opsd_load = pd . read_csv ( load , index_col = 0 , parse_dates = True ) . filter ( items = countries )
2018-08-03 09:53:14 +00:00
2020-12-03 11:49:04 +00:00
logger . info ( f " Load data scaled with scalling factor { scaling } . " )
opsd_load * = scaling
2019-02-05 22:02:59 +00:00
2022-09-16 13:04:04 +00:00
nuts3 = gpd . read_file ( 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 ]
2023-05-11 14:58:35 +00:00
transfer = shapes_to_shapes ( group , nuts3_cntry . geometry ) . T . tocsr ( )
2022-09-16 13:04:04 +00:00
gdp_n = pd . Series (
transfer . dot ( nuts3_cntry [ " gdp " ] . fillna ( 1.0 ) . values ) , index = group . index
)
pop_n = pd . Series (
transfer . dot ( nuts3_cntry [ " pop " ] . fillna ( 1.0 ) . 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
2018-08-03 09:53:14 +00:00
factors = normed ( 0.6 * normed ( gdp_n ) + 0.4 * normed ( pop_n ) )
2022-09-16 13:04:04 +00:00
return pd . DataFrame (
factors . values * l . values [ : , np . newaxis ] ,
index = l . index ,
columns = factors . index ,
)
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
2022-03-24 13:47:00 +00:00
def update_transmission_costs ( n , costs , length_factor = 1.0 ) :
2022-01-11 08:38:34 +00:00
# TODO: line length factor of lines is applied to lines and links.
# Separate the function to distinguish.
2022-09-16 13:04:04 +00:00
n . lines [ " capital_cost " ] = (
n . lines [ " length " ] * length_factor * costs . at [ " HVAC overhead " , " capital_cost " ]
)
2018-01-29 21:28:33 +00:00
2022-09-16 13:04:04 +00:00
if n . links . empty :
return
2019-02-13 18:03:57 +00:00
2022-09-16 13:04:04 +00:00
dc_b = n . links . carrier == " DC "
2021-03-10 17:16:09 +00:00
# If there are no dc links, then the 'underwater_fraction' column
# may be missing. Therefore we have to return here.
2022-09-16 13:04:04 +00:00
if n . links . loc [ dc_b ] . empty :
return
costs = (
n . links . loc [ dc_b , " length " ]
* length_factor
* (
( 1.0 - 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
2022-09-16 13:04:04 +00:00
def attach_wind_and_solar (
2023-05-03 11:24:57 +00:00
n , costs , input_profiles , carriers , extendable_carriers , line_length_factor = 1
2022-09-16 13:04:04 +00:00
) :
2023-06-30 11:25:20 +00:00
add_missing_carriers ( n , carriers )
2022-06-09 18:31:50 +00:00
2023-05-03 11:24:57 +00:00
for car in carriers :
if car == " hydro " :
2022-06-09 18:31:50 +00:00
continue
2018-01-29 21:28:33 +00:00
2023-05-03 11:24:57 +00:00
with xr . open_dataset ( getattr ( input_profiles , " profile_ " + car ) ) as ds :
2022-09-16 13:04:04 +00:00
if ds . indexes [ " bus " ] . empty :
continue
2023-05-03 11:24:57 +00:00
supcar = car . split ( " - " , 2 ) [ 0 ]
if supcar == " offwind " :
2022-09-16 13:04:04 +00:00
underwater_fraction = ds [ " underwater_fraction " ] . to_pandas ( )
connection_cost = (
line_length_factor
* ds [ " average_distance " ] . to_pandas ( )
* (
underwater_fraction
2023-05-03 11:24:57 +00:00
* costs . at [ car + " -connection-submarine " , " capital_cost " ]
2022-09-16 13:04:04 +00:00
+ ( 1.0 - underwater_fraction )
2023-05-03 11:24:57 +00:00
* costs . at [ car + " -connection-underground " , " capital_cost " ]
2022-09-16 13:04:04 +00:00
)
)
capital_cost = (
costs . at [ " offwind " , " capital_cost " ]
2023-05-03 11:24:57 +00:00
+ costs . at [ car + " -station " , " capital_cost " ]
2022-09-16 13:04:04 +00:00
+ connection_cost
)
logger . info (
" Added connection cost of {:0.0f} - {:0.0f} Eur/MW/a to {} " . format (
2023-05-03 11:24:57 +00:00
connection_cost . min ( ) , connection_cost . max ( ) , car
2022-09-16 13:04:04 +00:00
)
)
2018-12-19 09:30:25 +00:00
else :
2023-05-03 11:24:57 +00:00
capital_cost = costs . at [ car , " capital_cost " ]
2018-01-29 21:28:33 +00:00
2022-09-16 13:04:04 +00:00
n . madd (
" Generator " ,
ds . indexes [ " bus " ] ,
2023-05-03 11:24:57 +00:00
" " + car ,
2022-09-16 13:04:04 +00:00
bus = ds . indexes [ " bus " ] ,
2023-05-03 11:24:57 +00:00
carrier = car ,
p_nom_extendable = car in extendable_carriers [ " Generator " ] ,
2022-09-16 13:04:04 +00:00
p_nom_max = ds [ " p_nom_max " ] . to_pandas ( ) ,
weight = ds [ " weight " ] . to_pandas ( ) ,
2023-05-03 11:24:57 +00:00
marginal_cost = costs . at [ supcar , " marginal_cost " ] ,
2022-09-16 13:04:04 +00:00
capital_cost = capital_cost ,
2023-05-03 11:24:57 +00:00
efficiency = costs . at [ supcar , " efficiency " ] ,
2022-09-16 13:04:04 +00:00
p_max_pu = ds [ " profile " ] . transpose ( " time " , " bus " ) . to_pandas ( ) ,
)
def attach_conventional_generators (
n ,
costs ,
ppl ,
conventional_carriers ,
extendable_carriers ,
2023-05-17 17:25:45 +00:00
conventional_params ,
2022-09-16 13:04:04 +00:00
conventional_inputs ,
) :
2023-05-03 11:24:57 +00:00
carriers = list ( set ( conventional_carriers ) | set ( extendable_carriers [ " Generator " ] ) )
2023-06-30 11:25:20 +00:00
add_missing_carriers ( n , carriers )
2023-05-03 11:24:57 +00:00
add_co2_emissions ( n , costs , carriers )
2020-12-03 18:50:53 +00:00
2022-09-16 13:04:04 +00:00
ppl = (
ppl . query ( " carrier in @carriers " )
. join ( costs , on = " carrier " , rsuffix = " _r " )
. rename ( index = lambda s : " C " + str ( s ) )
)
2022-06-09 22:54:54 +00:00
ppl [ " efficiency " ] = ppl . efficiency . fillna ( ppl . efficiency_r )
2022-09-19 11:08:03 +00:00
ppl [ " marginal_cost " ] = (
ppl . carrier . map ( costs . VOM ) + ppl . carrier . map ( costs . fuel ) / ppl . efficiency
)
2018-07-10 14:29:11 +00:00
2022-09-16 13:04:04 +00:00
logger . info (
" Adding {} generators with capacities [GW] \n {} " . format (
len ( ppl ) , ppl . groupby ( " carrier " ) . p_nom . sum ( ) . div ( 1e3 ) . round ( 2 )
2022-06-09 18:31:50 +00:00
)
2022-09-16 13:04:04 +00:00
)
n . madd (
" Generator " ,
ppl . index ,
carrier = ppl . carrier ,
bus = ppl . bus ,
p_nom_min = ppl . p_nom . where ( ppl . carrier . isin ( conventional_carriers ) , 0 ) ,
p_nom = ppl . p_nom . where ( ppl . carrier . isin ( conventional_carriers ) , 0 ) ,
p_nom_extendable = ppl . carrier . isin ( extendable_carriers [ " Generator " ] ) ,
efficiency = ppl . efficiency ,
marginal_cost = ppl . marginal_cost ,
capital_cost = ppl . capital_cost ,
build_year = ppl . datein . fillna ( 0 ) . astype ( int ) ,
lifetime = ( ppl . dateout - ppl . datein ) . fillna ( np . inf ) ,
)
2023-05-17 17:25:45 +00:00
for carrier in conventional_params :
2022-04-08 13:41:23 +00:00
# Generators with technology affected
2022-06-09 18:31:50 +00:00
idx = n . generators . query ( " carrier == @carrier " ) . index
2022-06-28 08:14:26 +00:00
2023-05-17 17:25:45 +00:00
for attr in list ( set ( conventional_params [ carrier ] ) & set ( n . generators ) ) :
values = conventional_params [ carrier ] [ attr ]
2022-06-28 08:14:26 +00:00
2022-06-28 14:33:46 +00:00
if f " conventional_ { carrier } _ { attr } " in conventional_inputs :
2022-06-28 08:14:26 +00:00
# Values affecting generators of technology k country-specific
# First map generator buses to countries; then map countries to p_max_pu
2023-05-12 12:51:39 +00:00
values = pd . read_csv (
snakemake . input [ f " conventional_ { carrier } _ { attr } " ] , index_col = 0
) . iloc [ : , 0 ]
2022-06-28 08:14:26 +00:00
bus_values = n . buses . country . map ( values )
2022-09-16 13:04:04 +00:00
n . generators [ attr ] . update (
n . generators . loc [ idx ] . bus . map ( bus_values ) . dropna ( )
)
2022-06-28 08:14:26 +00:00
else :
# Single value affecting all generators of technology k indiscriminantely of country
2022-06-28 14:33:46 +00:00
n . generators . loc [ idx , attr ] = values
2022-04-08 13:41:23 +00:00
2018-01-29 21:28:33 +00:00
2023-05-17 17:25:45 +00:00
def attach_hydro ( n , costs , ppl , profile_hydro , hydro_capacities , carriers , * * params ) :
2023-06-30 11:25:20 +00:00
add_missing_carriers ( n , carriers )
2023-05-03 11:24:57 +00:00
add_co2_emissions ( n , costs , carriers )
2018-01-29 21:28:33 +00:00
2022-09-16 13:04:04 +00:00
ppl = (
ppl . query ( ' carrier == " hydro " ' )
. reset_index ( drop = True )
. rename ( index = lambda s : str ( s ) + " hydro " )
)
2019-10-30 22:09:41 +00:00
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
2022-09-16 13:04:04 +00:00
country = ppl [ " bus " ] . map ( n . buses . country ) . rename ( " country " )
2019-02-22 16:09:52 +00:00
2021-04-27 05:51:46 +00:00
inflow_idx = ror . index . union ( hydro . index )
2019-10-30 22:09:41 +00:00
if not inflow_idx . empty :
2022-09-16 13:04:04 +00:00
dist_key = ppl . loc [ inflow_idx , " p_nom " ] . groupby ( country ) . transform ( normed )
2018-01-29 21:28:33 +00:00
2021-09-14 14:34:02 +00:00
with xr . open_dataarray ( profile_hydro ) as inflow :
2019-10-30 22:09:41 +00:00
inflow_countries = pd . Index ( country [ inflow_idx ] )
2022-09-16 13:04:04 +00:00
missing_c = inflow_countries . unique ( ) . difference (
inflow . indexes [ " countries " ]
)
assert missing_c . empty , (
f " ' { 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.0 , other = 1.0 )
) ,
)
if " PHS " in carriers and not phs . empty :
2023-05-17 17:25:45 +00:00
# fill missing max hours to params value and
2020-12-03 18:50:53 +00:00
# assume no natural inflow due to lack of data
2023-05-17 17:25:45 +00:00
max_hours = params . get ( " PHS_max_hours " , 6 )
2022-09-16 13:04:04 +00:00
phs = phs . replace ( { " max_hours " : { 0 : 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 :
2023-05-17 17:25:45 +00:00
hydro_max_hours = params . get ( " hydro_max_hours " )
2022-01-11 08:38:34 +00:00
assert hydro_max_hours is not None , " No path for hydro capacities given. "
2022-09-16 13:04:04 +00:00
hydro_stats = pd . read_csv (
hydro_capacities , comment = " # " , na_values = " - " , index_col = 0
)
2019-11-01 12:27:42 +00:00
e_target = hydro_stats [ " E_store[TWh] " ] . clip ( lower = 0.2 ) * 1e6
2022-09-16 13:04:04 +00:00
e_installed = hydro . eval ( " p_nom * max_hours " ) . groupby ( hydro . country ) . sum ( )
2019-11-01 12:27:42 +00:00
e_missing = e_target - e_installed
2023-02-02 13:59:50 +00:00
missing_mh_i = hydro . query ( " max_hours.isnull() " ) . index
2019-11-01 12:27:42 +00:00
2022-09-16 13:04:04 +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
2022-09-16 13:04:04 +00:00
max_hours_country = (
e_missing / hydro . loc [ missing_mh_i ] . groupby ( " country " ) . p_nom . sum ( )
)
2019-11-01 12:27:42 +00:00
2022-09-16 13:04:04 +00:00
elif hydro_max_hours == " estimate_by_large_installations " :
max_hours_country = (
hydro_stats [ " E_store[TWh] " ] * 1e3 / hydro_stats [ " p_nom_discharge[GW] " ]
)
2019-11-01 12:27:42 +00:00
2023-02-07 12:20:06 +00:00
max_hours_country . clip ( 0 , inplace = True )
2022-09-16 13:04:04 +00:00
missing_countries = pd . Index ( hydro [ " country " ] . unique ( ) ) . difference (
max_hours_country . dropna ( ) . index
)
2019-11-01 12:27:42 +00:00
if not missing_countries . empty :
2022-09-16 13:04:04 +00:00
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 )
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 " ] ,
marginal_cost = costs . at [ " hydro " , " marginal_cost " ] ,
p_max_pu = 1.0 , # dispatch
p_min_pu = 0.0 , # store
efficiency_dispatch = costs . at [ " hydro " , " efficiency " ] ,
efficiency_store = 0.0 ,
cyclic_state_of_charge = True ,
inflow = inflow_t . loc [ : , hydro . index ] ,
)
2018-01-29 21:28:33 +00:00
2022-01-11 08:38:34 +00:00
def attach_extendable_generators ( n , costs , ppl , carriers ) :
2022-09-16 13:04:04 +00:00
logger . warning (
" The function `attach_extendable_generators` is deprecated in v0.5.0. "
)
2023-06-30 11:25:20 +00:00
add_missing_carriers ( n , carriers )
2023-05-03 11:24:57 +00:00
add_co2_emissions ( n , costs , carriers )
2018-01-29 21:28:33 +00:00
2019-02-15 17:27:21 +00:00
for tech in carriers :
2022-09-16 13:04:04 +00:00
if tech . startswith ( " OCGT " ) :
ocgt = (
ppl . query ( " carrier in [ ' OCGT ' , ' CCGT ' ] " )
. groupby ( " bus " , as_index = False )
. first ( )
)
n . madd (
" Generator " ,
ocgt . index ,
suffix = " OCGT " ,
bus = ocgt [ " bus " ] ,
carrier = tech ,
p_nom_extendable = True ,
p_nom = 0.0 ,
capital_cost = costs . at [ " OCGT " , " capital_cost " ] ,
marginal_cost = costs . at [ " OCGT " , " marginal_cost " ] ,
efficiency = costs . at [ " OCGT " , " efficiency " ] ,
)
elif tech . startswith ( " CCGT " ) :
ccgt = (
ppl . query ( " carrier in [ ' OCGT ' , ' CCGT ' ] " )
. groupby ( " bus " , as_index = False )
. first ( )
)
n . madd (
" Generator " ,
ccgt . index ,
suffix = " CCGT " ,
bus = ccgt [ " bus " ] ,
2019-11-19 11:26:01 +00:00
carrier = tech ,
p_nom_extendable = True ,
2022-09-16 13:04:04 +00:00
p_nom = 0.0 ,
capital_cost = costs . at [ " CCGT " , " capital_cost " ] ,
marginal_cost = costs . at [ " CCGT " , " marginal_cost " ] ,
efficiency = costs . at [ " CCGT " , " efficiency " ] ,
)
elif tech . startswith ( " nuclear " ) :
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.0 ,
capital_cost = costs . at [ " nuclear " , " capital_cost " ] ,
marginal_cost = costs . at [ " nuclear " , " marginal_cost " ] ,
efficiency = costs . at [ " nuclear " , " efficiency " ] ,
)
2019-11-19 11:26:01 +00:00
2019-02-15 17:27:21 +00:00
else :
2022-09-16 13:04:04 +00:00
raise NotImplementedError (
2023-03-07 16:21:00 +00:00
" Adding extendable generators for carrier "
2022-09-16 13:04:04 +00:00
" ' {tech} ' is not implemented, yet. "
" 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
2022-06-09 18:31:50 +00:00
def attach_OPSD_renewables ( n , tech_map ) :
tech_string = " , " . join ( sum ( tech_map . values ( ) , [ ] ) )
2022-09-16 13:04:04 +00:00
logger . info ( f " Using OPSD renewable capacities for carriers { tech_string } . " )
2020-12-03 22:13:41 +00:00
2022-06-07 13:17:49 +00:00
df = pm . data . OPSD_VRE ( ) . powerplant . convert_country_to_alpha2 ( )
2022-09-16 13:04:04 +00:00
technology_b = ~ df . Technology . isin ( [ " Onshore " , " Offshore " ] )
df [ " Fueltype " ] = df . Fueltype . where ( technology_b , df . Technology ) . replace (
{ " Solar " : " PV " }
)
df = df . query ( " Fueltype in @tech_map " ) . powerplant . convert_country_to_alpha2 ( )
2020-12-03 22:13:41 +00:00
2022-06-09 18:31:50 +00:00
for fueltype , carriers in tech_map . items ( ) :
gens = n . generators [ lambda df : df . carrier . isin ( carriers ) ]
2020-12-03 22:13:41 +00:00
buses = n . buses . loc [ gens . bus . unique ( ) ]
2022-09-16 13:04:04 +00:00
gens_per_bus = gens . groupby ( " bus " ) . p_nom . count ( )
2020-12-03 22:13:41 +00:00
2023-06-29 14:38:56 +00:00
caps = map_country_bus ( df . query ( " Fueltype == @fueltype and lat == lat " ) , buses )
2022-09-16 13:04:04 +00:00
caps = caps . groupby ( [ " bus " ] ) . Capacity . sum ( )
2020-12-03 22:13:41 +00:00
caps = caps / gens_per_bus . reindex ( caps . index , fill_value = 1 )
n . generators . p_nom . update ( gens . bus . map ( caps ) . dropna ( ) )
2021-06-30 19:07:38 +00:00
n . generators . p_nom_min . update ( gens . bus . map ( caps ) . dropna ( ) )
2020-12-03 22:13:41 +00:00
2023-06-15 16:52:25 +00:00
def estimate_renewable_capacities ( n , year , tech_map , expansion_limit , countries ) :
2022-09-16 13:04:04 +00:00
if not len ( countries ) or not len ( tech_map ) :
return
2019-02-05 22:00:35 +00:00
2022-04-04 17:03:09 +00:00
capacities = pm . data . IRENASTAT ( ) . powerplant . convert_country_to_alpha2 ( )
2022-09-16 13:04:04 +00:00
capacities = capacities . query (
" Year == @year and Technology in @tech_map and Country in @countries "
)
2022-04-04 17:03:09 +00:00
capacities = capacities . groupby ( [ " Technology " , " Country " ] ) . Capacity . sum ( )
2019-02-05 22:00:35 +00:00
2022-09-16 13:04:04 +00:00
logger . info (
f " Heuristics applied to distribute renewable capacities [GW]: "
f " \n { capacities . groupby ( ' Technology ' ) . sum ( ) . div ( 1e3 ) . round ( 2 ) } "
)
2019-02-05 22:00:35 +00:00
2022-04-04 17:03:09 +00:00
for ppm_technology , techs in tech_map . items ( ) :
2022-09-16 13:04:04 +00:00
tech_i = n . generators . query ( " carrier in @techs " ) . index
stats = capacities . loc [ ppm_technology ] . reindex ( countries , fill_value = 0.0 )
2022-06-09 18:31:50 +00:00
country = n . generators . bus [ tech_i ] . map ( n . buses . country )
2022-06-07 13:17:49 +00:00
existent = n . generators . p_nom [ tech_i ] . groupby ( country ) . sum ( )
missing = stats - existent
dist = n . generators_t . p_max_pu . mean ( ) * n . generators . p_nom_max
2022-09-16 13:04:04 +00:00
n . generators . loc [ tech_i , " p_nom " ] + = (
2022-06-07 13:17:49 +00:00
dist [ tech_i ]
. groupby ( country )
. transform ( lambda s : normed ( s ) * missing [ s . name ] )
2022-09-16 13:04:04 +00:00
. where ( lambda s : s > 0.1 , 0.0 ) # only capacities above 100kW
)
n . generators . loc [ tech_i , " p_nom_min " ] = n . generators . loc [ tech_i , " p_nom " ]
2018-01-29 21:28:33 +00:00
2022-04-04 17:03:09 +00:00
if expansion_limit :
assert np . isscalar ( expansion_limit )
2022-09-16 13:04:04 +00:00
logger . info (
f " Reducing capacity expansion limit to { expansion_limit * 100 : .2f } % of installed capacity. "
)
n . generators . loc [ tech_i , " p_nom_max " ] = (
expansion_limit * n . generators . loc [ tech_i , " p_nom_min " ]
)
2022-04-04 17:03:09 +00:00
2020-12-03 18:50:53 +00:00
2018-01-29 21:28:33 +00:00
if __name__ == " __main__ " :
2022-09-16 13:04:04 +00:00
if " snakemake " not in globals ( ) :
2019-12-09 20:29:15 +00:00
from _helpers import mock_snakemake
2022-09-16 13:04:04 +00:00
snakemake = mock_snakemake ( " add_electricity " )
2019-11-28 07:22:52 +00:00
configure_logging ( snakemake )
2018-01-29 21:28:33 +00:00
2023-06-15 16:52:25 +00:00
params = snakemake . params
2022-01-24 18:48:26 +00:00
n = pypsa . Network ( snakemake . input . base_network )
2022-09-16 13:04:04 +00:00
Nyears = n . snapshot_weightings . objective . sum ( ) / 8760.0
costs = load_costs (
snakemake . input . tech_costs ,
2023-06-15 16:52:25 +00:00
params . costs ,
params . electricity [ " max_hours " ] ,
2022-09-16 13:04:04 +00:00
Nyears ,
)
2022-01-24 18:48:26 +00:00
ppl = load_powerplants ( snakemake . input . powerplants )
2022-01-11 08:38:34 +00:00
2022-09-16 13:04:04 +00:00
attach_load (
n ,
snakemake . input . regions ,
snakemake . input . load ,
snakemake . input . nuts3_shapes ,
2023-06-15 16:52:25 +00:00
params . countries ,
params . scaling_factor ,
2022-09-16 13:04:04 +00:00
)
2023-06-15 16:52:25 +00:00
update_transmission_costs ( n , costs , params . length_factor )
2022-09-16 13:04:04 +00:00
2023-06-15 17:12:30 +00:00
renewable_carriers = set ( params . electricity [ " renewable_carriers " ] )
2023-06-15 16:52:25 +00:00
extendable_carriers = params . electricity [ " extendable_carriers " ]
conventional_carriers = params . electricity [ " conventional_carriers " ]
2022-09-16 13:04:04 +00:00
conventional_inputs = {
k : v for k , v in snakemake . input . items ( ) if k . startswith ( " conventional_ " )
}
attach_conventional_generators (
n ,
costs ,
ppl ,
conventional_carriers ,
extendable_carriers ,
2023-06-15 16:52:25 +00:00
params . conventional ,
2022-09-16 13:04:04 +00:00
conventional_inputs ,
)
attach_wind_and_solar (
n ,
costs ,
snakemake . input ,
renewable_carriers ,
extendable_carriers ,
2023-06-15 16:52:25 +00:00
params . length_factor ,
2022-09-16 13:04:04 +00:00
)
if " hydro " in renewable_carriers :
2023-06-15 16:52:25 +00:00
para = params . renewable [ " hydro " ]
2022-09-16 13:04:04 +00:00
attach_hydro (
n ,
costs ,
ppl ,
snakemake . input . profile_hydro ,
snakemake . input . hydro_capacities ,
2023-05-17 17:25:45 +00:00
para . pop ( " carriers " , [ ] ) ,
* * para ,
2022-09-16 13:04:04 +00:00
)
2023-06-15 16:52:25 +00:00
estimate_renewable_caps = params . electricity [ " estimate_renewable_capacities " ]
2022-09-16 13:04:04 +00:00
if estimate_renewable_caps [ " enable " ] :
2023-06-15 16:52:25 +00:00
tech_map = estimate_renewable_caps [ " technology_mapping " ]
expansion_limit = estimate_renewable_caps [ " expansion_limit " ]
year = estimate_renewable_caps [ " year " ]
2022-06-09 18:31:50 +00:00
if estimate_renewable_caps [ " from_opsd " ] :
attach_OPSD_renewables ( n , tech_map )
2023-05-17 17:26:00 +00:00
estimate_renewable_capacities (
2023-06-15 16:52:25 +00:00
n , year , tech_map , expansion_limit , params . countries
2023-05-17 17:26:00 +00:00
)
2022-06-07 13:17:49 +00:00
2021-06-30 19:07:38 +00:00
update_p_nom_max ( n )
2023-04-25 15:26:23 +00:00
2023-05-03 11:24:57 +00:00
sanitize_carriers ( n , snakemake . config )
2019-02-05 22:00:35 +00:00
2022-06-30 06:39:03 +00:00
n . meta = snakemake . config
2022-01-24 18:48:26 +00:00
n . export_to_netcdf ( snakemake . output [ 0 ] )