2019-04-17 12:24:22 +00:00
# -*- coding: utf-8 -*-
2023-03-06 17:49:23 +00:00
# SPDX-FileCopyrightText: : 2020-2023 The PyPSA-Eur Authors
#
# SPDX-License-Identifier: MIT
2023-03-09 11:45:43 +00:00
"""
Adds all sector - coupling components to the network , including demand and supply
technologies for the buildings , transport and industry sectors .
"""
2021-07-01 18:09:04 +00:00
import logging
import os
2023-03-06 08:27:45 +00:00
import re
2021-07-01 18:09:04 +00:00
from itertools import product
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
import networkx as nx
2019-04-17 12:24:22 +00:00
import numpy as np
2023-02-16 17:42:19 +00:00
import pandas as pd
2021-07-01 18:09:04 +00:00
import pypsa
2019-04-17 12:24:22 +00:00
import xarray as xr
2023-07-13 20:35:03 +00:00
from _helpers import generate_periodic_profiles , update_config_with_sector_opts
2023-05-15 13:02:39 +00:00
from add_electricity import calculate_annuity , sanitize_carriers
2023-03-06 18:18:17 +00:00
from build_energy_totals import build_co2_totals , build_eea_co2 , build_eurostat_co2
2021-11-04 20:48:54 +00:00
from networkx . algorithms import complement
from networkx . algorithms . connectivity . edge_augmentation import k_edge_augmentation
from pypsa . geo import haversine_pts
2022-08-01 16:03:11 +00:00
from pypsa . io import import_components_from_dataframe
2021-07-01 18:09:04 +00:00
from scipy . stats import beta
2021-11-04 20:48:54 +00:00
2021-07-01 18:09:04 +00:00
logger = logging . getLogger ( __name__ )
2019-04-17 12:24:22 +00:00
2021-08-04 07:48:23 +00:00
from types import SimpleNamespace
2023-03-06 08:27:45 +00:00
2021-08-04 07:48:23 +00:00
spatial = SimpleNamespace ( )
2022-08-02 07:27:37 +00:00
from packaging . version import Version , parse
2023-03-06 08:27:45 +00:00
2022-08-02 07:27:37 +00:00
pd_version = parse ( pd . __version__ )
agg_group_kwargs = dict ( numeric_only = False ) if pd_version > = Version ( " 1.3 " ) else { }
2021-08-04 07:48:23 +00:00
2023-03-06 08:27:45 +00:00
2022-03-18 09:18:24 +00:00
def define_spatial ( nodes , options ) :
2021-08-04 07:48:23 +00:00
"""
Namespace for spatial .
2021-09-29 12:37:36 +00:00
2021-08-04 07:48:23 +00:00
Parameters
- - - - - - - - - -
nodes : list - like
"""
global spatial
spatial . nodes = nodes
2021-09-28 09:33:21 +00:00
# biomass
2021-07-12 10:31:18 +00:00
spatial . biomass = SimpleNamespace ( )
2023-01-24 17:44:39 +00:00
if options . get ( " biomass_spatial " , options [ " biomass_transport " ] ) :
2021-07-12 10:31:18 +00:00
spatial . biomass . nodes = nodes + " solid biomass "
spatial . biomass . locations = nodes
spatial . biomass . industry = nodes + " solid biomass for industry "
spatial . biomass . industry_cc = nodes + " solid biomass for industry CC "
else :
spatial . biomass . nodes = [ " EU solid biomass " ]
2021-08-09 15:51:37 +00:00
spatial . biomass . locations = [ " EU " ]
2021-07-12 10:31:18 +00:00
spatial . biomass . industry = [ " solid biomass for industry " ]
spatial . biomass . industry_cc = [ " solid biomass for industry CC " ]
spatial . biomass . df = pd . DataFrame ( vars ( spatial . biomass ) , index = nodes )
2021-09-29 12:37:36 +00:00
2021-09-28 09:33:21 +00:00
# co2
2021-07-09 10:50:40 +00:00
spatial . co2 = SimpleNamespace ( )
2021-09-29 12:37:36 +00:00
2023-02-08 21:57:01 +00:00
if options [ " co2_spatial " ] :
2021-07-09 10:50:40 +00:00
spatial . co2 . nodes = nodes + " co2 stored "
spatial . co2 . locations = nodes
spatial . co2 . vents = nodes + " co2 vent "
2023-02-06 08:46:24 +00:00
spatial . co2 . process_emissions = nodes + " process emissions "
2021-07-09 10:50:40 +00:00
else :
spatial . co2 . nodes = [ " co2 stored " ]
2021-08-06 10:46:03 +00:00
spatial . co2 . locations = [ " EU " ]
2021-07-09 10:50:40 +00:00
spatial . co2 . vents = [ " co2 vent " ]
2023-02-06 08:46:24 +00:00
spatial . co2 . process_emissions = [ " process emissions " ]
2021-07-09 10:50:40 +00:00
spatial . co2 . df = pd . DataFrame ( vars ( spatial . co2 ) , index = nodes )
2022-03-18 09:18:24 +00:00
2021-11-02 18:03:26 +00:00
# gas
2021-08-04 07:48:23 +00:00
spatial . gas = SimpleNamespace ( )
if options [ " gas_network " ] :
spatial . gas . nodes = nodes + " gas "
spatial . gas . locations = nodes
2021-08-04 12:47:13 +00:00
spatial . gas . biogas = nodes + " biogas "
spatial . gas . industry = nodes + " gas for industry "
spatial . gas . industry_cc = nodes + " gas for industry CC "
2021-12-03 16:22:06 +00:00
spatial . gas . biogas_to_gas = nodes + " biogas to gas "
2021-08-04 07:48:23 +00:00
else :
spatial . gas . nodes = [ " EU gas " ]
2021-12-03 16:22:06 +00:00
spatial . gas . locations = [ " EU " ]
2021-08-04 12:47:13 +00:00
spatial . gas . biogas = [ " EU biogas " ]
spatial . gas . industry = [ " gas for industry " ]
2021-12-03 16:22:06 +00:00
spatial . gas . biogas_to_gas = [ " EU biogas to gas " ]
2023-01-24 17:44:39 +00:00
if options . get ( " co2_spatial " , options [ " co2network " ] ) :
spatial . gas . industry_cc = nodes + " gas for industry CC "
else :
spatial . gas . industry_cc = [ " gas for industry CC " ]
2021-08-04 07:48:23 +00:00
spatial . gas . df = pd . DataFrame ( vars ( spatial . gas ) , index = nodes )
2022-06-10 14:53:37 +00:00
# ammonia
2022-06-23 13:24:44 +00:00
if options . get ( " ammonia " ) :
spatial . ammonia = SimpleNamespace ( )
if options . get ( " ammonia " ) == " regional " :
spatial . ammonia . nodes = nodes + " NH3 "
spatial . ammonia . locations = nodes
else :
spatial . ammonia . nodes = [ " EU NH3 " ]
spatial . ammonia . locations = [ " EU " ]
spatial . ammonia . df = pd . DataFrame ( vars ( spatial . ammonia ) , index = nodes )
2022-06-10 14:53:37 +00:00
2022-11-13 17:25:32 +00:00
# hydrogen
spatial . h2 = SimpleNamespace ( )
spatial . h2 . nodes = nodes + " H2 "
spatial . h2 . locations = nodes
# methanol
spatial . methanol = SimpleNamespace ( )
2023-10-24 14:46:58 +00:00
if options [ " co2_budget_national " ] :
spatial . methanol . nodes = nodes + " methanol "
spatial . methanol . locations = nodes
spatial . methanol . shipping = nodes + " shipping methanol "
else :
spatial . methanol . nodes = [ " EU methanol " ]
spatial . methanol . locations = [ " EU " ]
spatial . methanol . shipping = [ " EU shipping methanol " ]
2022-11-13 17:25:32 +00:00
2022-03-18 09:18:24 +00:00
# oil
spatial . oil = SimpleNamespace ( )
2023-10-24 14:46:58 +00:00
if options [ " co2_budget_national " ] :
spatial . oil . nodes = nodes + " oil "
spatial . oil . locations = nodes
spatial . oil . naphtha = nodes + " naphtha for industry "
spatial . oil . kerosene = nodes + " kerosene for aviation "
spatial . oil . shipping = nodes + " shipping oil "
spatial . oil . agriculture_machinery = nodes + " agriculture machinery oil "
else :
spatial . oil . nodes = [ " EU oil " ]
spatial . oil . locations = [ " EU " ]
spatial . oil . naphtha = [ " EU naphtha for industry " ]
spatial . oil . kerosene = [ " EU kerosene for aviation " ]
spatial . oil . shipping = [ " EU shipping oil " ]
spatial . oil . agriculture_machinery = [ " EU agriculture machinery oil " ]
2023-10-24 12:06:17 +00:00
spatial . oil . land_transport = nodes + " land transport oil "
2022-03-18 09:18:24 +00:00
# uranium
spatial . uranium = SimpleNamespace ( )
spatial . uranium . nodes = [ " EU uranium " ]
spatial . uranium . locations = [ " EU " ]
# coal
spatial . coal = SimpleNamespace ( )
spatial . coal . nodes = [ " EU coal " ]
spatial . coal . locations = [ " EU " ]
# lignite
spatial . lignite = SimpleNamespace ( )
spatial . lignite . nodes = [ " EU lignite " ]
spatial . lignite . locations = [ " EU " ]
2022-03-18 12:46:40 +00:00
return spatial
2021-08-04 07:48:23 +00:00
2021-11-02 18:03:26 +00:00
from types import SimpleNamespace
2023-03-06 08:27:45 +00:00
2021-11-02 18:03:26 +00:00
spatial = SimpleNamespace ( )
2021-07-12 10:31:18 +00:00
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
def emission_sectors_from_opts ( opts ) :
sectors = [ " electricity " ]
if " T " in opts :
sectors + = [ " rail non-elec " , " road non-elec " ]
if " H " in opts :
sectors + = [ " residential non-elec " , " services non-elec " ]
if " I " in opts :
sectors + = [
" industrial non-elec " ,
" industrial processes " ,
" domestic aviation " ,
" international aviation " ,
" domestic navigation " ,
" international navigation " ,
]
2021-07-06 16:32:35 +00:00
if " A " in opts :
2021-07-06 16:36:04 +00:00
sectors + = [ " agriculture " ]
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
return sectors
2019-04-17 12:24:22 +00:00
2020-07-18 09:22:30 +00:00
2021-07-01 18:09:04 +00:00
def get ( item , investment_year = None ) :
"""
Check whether item depends on investment year .
"""
2023-10-08 09:20:36 +00:00
return item [ investment_year ] if isinstance ( item , dict ) else item
2020-12-30 14:55:08 +00:00
2021-02-04 08:15:03 +00:00
2022-08-02 06:52:48 +00:00
def co2_emissions_year (
2023-08-09 12:35:39 +00:00
countries , input_eurostat , opts , emissions_scope , report_year , input_co2 , year
2022-08-02 06:52:48 +00:00
) :
2020-12-30 14:55:08 +00:00
"""
2021-04-23 09:26:38 +00:00
Calculate CO2 emissions in one specific year ( e . g . 1990 or 2018 ) .
2020-12-30 14:55:08 +00:00
"""
2023-08-09 12:35:39 +00:00
eea_co2 = build_eea_co2 ( input_co2 , year , emissions_scope )
2021-01-25 13:17:31 +00:00
2021-07-01 18:09:04 +00:00
# TODO: read Eurostat data from year > 2014
# this only affects the estimation of CO2 emissions for BA, RS, AL, ME, MK
2020-12-30 14:55:08 +00:00
if year > 2014 :
2022-08-01 13:21:11 +00:00
eurostat_co2 = build_eurostat_co2 (
input_eurostat , countries , report_year , year = 2014
)
2020-12-30 14:55:08 +00:00
else :
2022-08-01 13:21:11 +00:00
eurostat_co2 = build_eurostat_co2 ( input_eurostat , countries , report_year , year )
2021-01-25 13:17:31 +00:00
2022-08-01 13:21:11 +00:00
co2_totals = build_co2_totals ( countries , eea_co2 , eurostat_co2 )
2020-12-30 14:55:08 +00:00
2021-07-01 18:09:04 +00:00
sectors = emission_sectors_from_opts ( opts )
2020-12-30 14:55:08 +00:00
2021-07-01 18:09:04 +00:00
co2_emissions = co2_totals . loc [ countries , sectors ] . sum ( ) . sum ( )
# convert MtCO2 to GtCO2
2021-07-08 12:41:34 +00:00
co2_emissions * = 0.001
2021-06-18 07:45:29 +00:00
2020-12-30 14:55:08 +00:00
return co2_emissions
2021-07-01 18:09:04 +00:00
# TODO: move to own rule with sector-opts wildcard?
2022-08-02 06:52:48 +00:00
def build_carbon_budget ( o , input_eurostat , fn , emissions_scope , report_year ) :
2021-07-01 18:09:04 +00:00
"""
Distribute carbon budget following beta or exponential transition path .
"""
# opts?
2021-01-25 13:17:31 +00:00
if " be " in o :
2020-12-30 14:55:08 +00:00
# beta decay
carbon_budget = float ( o [ o . find ( " cb " ) + 2 : o . find ( " be " ) ] )
2021-07-01 18:09:04 +00:00
be = float ( o [ o . find ( " be " ) + 2 : ] )
2021-01-25 13:17:31 +00:00
if " ex " in o :
2020-12-30 14:55:08 +00:00
# exponential decay
carbon_budget = float ( o [ o . find ( " cb " ) + 2 : o . find ( " ex " ) ] )
2021-07-01 18:09:04 +00:00
r = float ( o [ o . find ( " ex " ) + 2 : ] )
2021-01-25 13:17:31 +00:00
2023-06-15 16:52:25 +00:00
countries = snakemake . params . countries
2021-01-14 09:06:29 +00:00
2022-08-02 06:52:48 +00:00
e_1990 = co2_emissions_year (
2023-08-09 13:04:35 +00:00
countries ,
input_eurostat ,
opts ,
emissions_scope ,
report_year ,
input_co2 ,
year = 1990 ,
2022-08-02 06:52:48 +00:00
)
2021-01-25 13:17:31 +00:00
2020-12-30 14:55:08 +00:00
# emissions at the beginning of the path (last year available 2018)
2022-08-02 06:52:48 +00:00
e_0 = co2_emissions_year (
2023-08-09 13:04:35 +00:00
countries ,
input_eurostat ,
opts ,
emissions_scope ,
report_year ,
input_co2 ,
year = 2018 ,
2022-08-02 06:52:48 +00:00
)
2021-07-08 12:41:34 +00:00
2023-06-15 16:52:25 +00:00
planning_horizons = snakemake . params . planning_horizons
2020-12-30 14:55:08 +00:00
t_0 = planning_horizons [ 0 ]
2021-07-01 18:09:04 +00:00
2021-01-25 13:17:31 +00:00
if " be " in o :
2021-07-01 18:09:04 +00:00
# final year in the path
2021-07-08 12:41:34 +00:00
t_f = t_0 + ( 2 * carbon_budget / e_0 ) . round ( 0 )
2021-07-01 18:09:04 +00:00
def beta_decay ( t ) :
cdf_term = ( t - t_0 ) / ( t_f - t_0 )
return ( e_0 / e_1990 ) * ( 1 - beta . cdf ( cdf_term , be , be ) )
2020-12-30 14:55:08 +00:00
# emissions (relative to 1990)
2021-07-01 18:09:04 +00:00
co2_cap = pd . Series ( { t : beta_decay ( t ) for t in planning_horizons } , name = o )
2021-01-25 13:17:31 +00:00
if " ex " in o :
2021-07-01 18:09:04 +00:00
T = carbon_budget / e_0
m = ( 1 + np . sqrt ( 1 + r * T ) ) / T
2021-02-04 08:15:03 +00:00
2021-07-01 18:09:04 +00:00
def exponential_decay ( t ) :
return ( e_0 / e_1990 ) * ( 1 + ( m + r ) * ( t - t_0 ) ) * np . exp ( - m * ( t - t_0 ) )
2021-01-25 13:17:31 +00:00
2021-07-01 18:09:04 +00:00
co2_cap = pd . Series (
{ t : exponential_decay ( t ) for t in planning_horizons } , name = o
)
2021-02-04 08:15:03 +00:00
2021-07-01 18:09:04 +00:00
# TODO log in Snakefile
2022-08-01 13:21:11 +00:00
csvs_folder = fn . rsplit ( " / " , 1 ) [ 0 ]
if not os . path . exists ( csvs_folder ) :
os . makedirs ( csvs_folder )
2021-07-01 18:09:04 +00:00
co2_cap . to_csv ( fn , float_format = " %.3f " )
def add_lifetime_wind_solar ( n , costs ) :
"""
Add lifetime for solar and wind generators .
"""
for carrier in [ " solar " , " onwind " , " offwind " ] :
gen_i = n . generators . index . str . contains ( carrier )
n . generators . loc [ gen_i , " lifetime " ] = costs . at [ carrier , " lifetime " ]
2020-08-19 10:41:17 +00:00
2021-11-04 20:48:54 +00:00
def haversine ( p ) :
coord0 = n . buses . loc [ p . bus0 , [ " x " , " y " ] ] . values
coord1 = n . buses . loc [ p . bus1 , [ " x " , " y " ] ] . values
return 1.5 * haversine_pts ( coord0 , coord1 )
def create_network_topology (
n , prefix , carriers = [ " DC " ] , connector = " -> " , bidirectional = True
) :
2020-10-20 11:46:39 +00:00
"""
2021-11-04 20:48:54 +00:00
Create a network topology from transmission lines and link carrier
selection .
2021-09-29 12:37:36 +00:00
2021-07-12 10:31:18 +00:00
Parameters
- - - - - - - - - -
n : pypsa . Network
prefix : str
2021-11-04 20:48:54 +00:00
carriers : list - like
2021-07-12 10:31:18 +00:00
connector : str
bidirectional : bool , default True
True : one link for each connection
False : one link for each connection and direction ( back and forth )
2021-09-29 12:37:36 +00:00
2021-07-12 10:31:18 +00:00
Returns
- - - - - - -
2021-11-04 20:48:54 +00:00
pd . DataFrame with columns bus0 , bus1 , length , underwater_fraction
2020-10-20 11:46:39 +00:00
"""
2021-07-12 10:31:18 +00:00
ln_attrs = [ " bus0 " , " bus1 " , " length " ]
lk_attrs = [ " bus0 " , " bus1 " , " length " , " underwater_fraction " ]
2022-04-11 15:08:25 +00:00
lk_attrs = n . links . columns . intersection ( lk_attrs )
2020-10-20 11:46:39 +00:00
2021-07-12 10:31:18 +00:00
candidates = pd . concat (
2021-11-04 20:48:54 +00:00
[ n . lines [ ln_attrs ] , n . links . loc [ n . links . carrier . isin ( carriers ) , lk_attrs ] ]
2021-07-12 10:31:18 +00:00
) . fillna ( 0 )
2020-10-20 11:46:39 +00:00
2021-11-04 20:48:54 +00:00
# base network topology purely on location not carrier
candidates [ " bus0 " ] = candidates . bus0 . map ( n . buses . location )
candidates [ " bus1 " ] = candidates . bus1 . map ( n . buses . location )
2020-10-20 11:46:39 +00:00
positive_order = candidates . bus0 < candidates . bus1
candidates_p = candidates [ positive_order ]
2021-07-12 10:31:18 +00:00
swap_buses = { " bus0 " : " bus1 " , " bus1 " : " bus0 " }
candidates_n = candidates [ ~ positive_order ] . rename ( columns = swap_buses )
candidates = pd . concat ( [ candidates_p , candidates_n ] )
2021-09-29 12:37:36 +00:00
2021-07-12 10:31:18 +00:00
def make_index ( c ) :
return prefix + c . bus0 + connector + c . bus1
2020-10-20 11:46:39 +00:00
topo = candidates . groupby ( [ " bus0 " , " bus1 " ] , as_index = False ) . mean ( )
2021-07-12 10:31:18 +00:00
topo . index = topo . apply ( make_index , axis = 1 )
2021-09-29 12:37:36 +00:00
2021-07-12 10:31:18 +00:00
if not bidirectional :
topo_reverse = topo . copy ( )
topo_reverse . rename ( columns = swap_buses , inplace = True )
topo_reverse . index = topo_reverse . apply ( make_index , axis = 1 )
2022-04-12 12:37:05 +00:00
topo = pd . concat ( [ topo , topo_reverse ] )
2021-09-29 12:37:36 +00:00
2020-10-20 11:46:39 +00:00
return topo
2021-07-01 18:09:04 +00:00
# TODO merge issue with PyPSA-Eur
def update_wind_solar_costs ( n , costs ) :
2020-08-12 09:08:09 +00:00
"""
2020-08-14 07:11:19 +00:00
Update costs for wind and solar generators added with pypsa - eur to those
2020-08-12 09:08:09 +00:00
cost in the planning year .
"""
2020-08-19 10:41:17 +00:00
# NB: solar costs are also manipulated for rooftop
# when distribution grid is inserted
2021-07-01 18:09:04 +00:00
n . generators . loc [ n . generators . carrier == " solar " , " capital_cost " ] = costs . at [
" solar-utility " , " fixed "
]
2020-08-19 10:41:17 +00:00
2021-07-01 18:09:04 +00:00
n . generators . loc [ n . generators . carrier == " onwind " , " capital_cost " ] = costs . at [
" onwind " , " fixed "
]
2020-08-19 10:41:17 +00:00
# for offshore wind, need to calculated connection costs
2020-08-12 09:08:09 +00:00
# assign clustered bus
# map initial network -> simplified network
2020-10-02 10:20:55 +00:00
busmap_s = pd . read_csv ( snakemake . input . busmap_s , index_col = 0 ) . squeeze ( )
2020-10-21 12:31:37 +00:00
busmap_s . index = busmap_s . index . astype ( str )
busmap_s = busmap_s . astype ( str )
2020-08-12 09:08:09 +00:00
# map simplified network -> clustered network
2020-10-02 10:20:55 +00:00
busmap = pd . read_csv ( snakemake . input . busmap , index_col = 0 ) . squeeze ( )
2020-10-21 12:31:37 +00:00
busmap . index = busmap . index . astype ( str )
busmap = busmap . astype ( str )
2020-09-21 15:04:45 +00:00
# map initial network -> clustered network
2020-08-12 09:08:09 +00:00
clustermaps = busmap_s . map ( busmap )
2020-08-19 10:41:17 +00:00
# code adapted from pypsa-eur/scripts/add_electricity.py
2021-07-01 18:09:04 +00:00
for connection in [ " dc " , " ac " ] :
2020-08-19 10:41:17 +00:00
tech = " offwind- " + connection
profile = snakemake . input [ " profile_offwind_ " + connection ]
with xr . open_dataset ( profile ) as ds :
underwater_fraction = ds [ " underwater_fraction " ] . to_pandas ( )
connection_cost = (
2023-06-15 16:52:25 +00:00
snakemake . params . length_factor
2020-08-19 10:41:17 +00:00
* ds [ " average_distance " ] . to_pandas ( )
* (
underwater_fraction
* costs . at [ tech + " -connection-submarine " , " fixed " ]
+ ( 1.0 - underwater_fraction )
* costs . at [ tech + " -connection-underground " , " fixed " ]
)
2023-03-06 08:27:45 +00:00
)
2020-08-19 10:41:17 +00:00
# convert to aggregated clusters with weighting
weight = ds [ " weight " ] . to_pandas ( )
2023-03-06 08:27:45 +00:00
2020-09-21 15:04:45 +00:00
# e.g. clusters == 37m means that VRE generators are left
# at clustering of simplified network, but that they are
# connected to 37-node network
2023-10-08 09:20:57 +00:00
genmap = (
busmap_s if snakemake . wildcards . clusters [ - 1 : ] == " m " else clustermaps
)
2020-09-21 15:04:45 +00:00
connection_cost = ( connection_cost * weight ) . groupby (
genmap
) . sum ( ) / weight . groupby ( genmap ) . sum ( )
2020-08-19 10:41:17 +00:00
capital_cost = (
costs . at [ " offwind " , " fixed " ]
+ costs . at [ tech + " -station " , " fixed " ]
+ connection_cost
)
logger . info (
" Added connection cost of {:0.0f} - {:0.0f} Eur/MW/a to {} " . format (
connection_cost [ 0 ] . min ( ) , connection_cost [ 0 ] . max ( ) , tech
)
2023-03-06 08:27:45 +00:00
)
2020-08-19 10:41:17 +00:00
2021-07-01 18:09:04 +00:00
n . generators . loc [
n . generators . carrier == tech , " capital_cost "
] = capital_cost . rename ( index = lambda node : node + " " + tech )
2020-08-19 10:41:17 +00:00
2020-08-14 07:11:19 +00:00
2021-07-02 13:00:19 +00:00
def add_carrier_buses ( n , carrier , nodes = None ) :
2020-07-18 09:22:30 +00:00
"""
2020-08-10 18:30:29 +00:00
Add buses to connect e . g . coal , nuclear and oil plants .
2020-07-18 09:22:30 +00:00
"""
2021-07-02 13:00:19 +00:00
if nodes is None :
2022-03-18 12:46:40 +00:00
nodes = vars ( spatial ) [ carrier ] . nodes
location = vars ( spatial ) [ carrier ] . locations
2020-07-29 13:50:40 +00:00
2021-07-02 13:00:19 +00:00
# skip if carrier already exists
if carrier in n . carriers . index :
return
2020-07-29 13:50:40 +00:00
2021-12-03 16:22:06 +00:00
if not isinstance ( nodes , pd . Index ) :
nodes = pd . Index ( nodes )
2021-07-02 13:00:19 +00:00
n . add ( " Carrier " , carrier )
2020-07-29 13:50:40 +00:00
2022-06-30 15:15:41 +00:00
unit = " MWh_LHV " if carrier == " gas " else " MWh_th "
2022-06-29 06:57:08 +00:00
2022-03-18 12:46:40 +00:00
n . madd ( " Bus " , nodes , location = location , carrier = carrier , unit = unit )
2020-07-29 13:50:40 +00:00
2021-07-02 13:00:19 +00:00
# capital cost could be corrected to e.g. 0.2 EUR/kWh * annuity and O&M
n . madd (
" Store " ,
nodes + " Store " ,
bus = nodes ,
e_nom_extendable = True ,
e_cyclic = True ,
carrier = carrier ,
2023-02-08 21:57:01 +00:00
capital_cost = 0.2
* costs . at [ carrier , " discount rate " ] , # preliminary value to avoid zeros
2021-07-02 13:00:19 +00:00
)
n . madd (
" Generator " ,
nodes ,
bus = nodes ,
p_nom_extendable = True ,
carrier = carrier ,
marginal_cost = costs . at [ carrier , " fuel " ] ,
)
2020-07-29 13:50:40 +00:00
2021-07-01 18:09:04 +00:00
# TODO: PyPSA-Eur merge issue
2019-04-18 13:23:37 +00:00
def remove_elec_base_techs ( n ) :
"""
Remove conventional generators ( e . g . OCGT ) and storage units ( e . g .
batteries and H2 ) from base electricity - only network , since they ' re added
here differently using links .
"""
2023-06-15 16:52:25 +00:00
for c in n . iterate_components ( snakemake . params . pypsa_eur ) :
to_keep = snakemake . params . pypsa_eur [ c . name ]
2021-04-29 15:11:10 +00:00
to_remove = pd . Index ( c . df . carrier . unique ( ) ) . symmetric_difference ( to_keep )
2023-02-16 17:42:19 +00:00
if to_remove . empty :
continue
logger . info ( f " Removing { c . list_name } with carrier { list ( to_remove ) } " )
2020-12-09 14:18:01 +00:00
names = c . df . index [ c . df . carrier . isin ( to_remove ) ]
n . mremove ( c . name , names )
n . carriers . drop ( to_remove , inplace = True , errors = " ignore " )
2019-04-18 13:23:37 +00:00
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
# TODO: PyPSA-Eur merge issue
2020-12-02 12:03:13 +00:00
def remove_non_electric_buses ( n ) :
"""
Remove buses from pypsa - eur with carriers which are not AC buses .
"""
2023-10-08 09:20:57 +00:00
if to_drop := list ( n . buses . query ( " carrier not in [ ' AC ' , ' DC ' ] " ) . carrier . unique ( ) ) :
2023-02-16 17:42:19 +00:00
logger . info ( f " Drop buses from PyPSA-Eur with carrier: { to_drop } " )
n . buses = n . buses [ n . buses . carrier . isin ( [ " AC " , " DC " ] ) ]
2020-12-02 12:03:13 +00:00
2021-07-01 18:09:04 +00:00
def patch_electricity_network ( n ) :
remove_elec_base_techs ( n )
remove_non_electric_buses ( n )
update_wind_solar_costs ( n , costs )
n . loads [ " carrier " ] = " electricity "
n . buses [ " location " ] = n . buses . index
2022-06-28 16:31:45 +00:00
n . buses [ " unit " ] = " MWh_el "
2021-09-27 09:16:12 +00:00
# remove trailing white space of load index until new PyPSA version after v0.18.
n . loads . rename ( lambda x : x . strip ( ) , inplace = True )
n . loads_t . p_set . rename ( lambda x : x . strip ( ) , axis = 1 , inplace = True )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
def add_co2_tracking ( n , options ) :
# minus sign because opposite to how fossil fuels used:
# CH4 burning puts CH4 down, atmosphere up
n . add ( " Carrier " , " co2 " , co2_emissions = - 1.0 )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
# this tracks CO2 in the atmosphere
n . add ( " Bus " , " co2 atmosphere " , location = " EU " , carrier = " co2 " , unit = " t_co2 " )
# can also be negative
n . add (
" Store " ,
" co2 atmosphere " ,
e_nom_extendable = True ,
e_min_pu = - 1 ,
carrier = " co2 " ,
bus = " co2 atmosphere " ,
)
# this tracks CO2 stored, e.g. underground
2021-07-07 15:58:47 +00:00
n . madd (
" Bus " ,
2021-07-09 10:50:40 +00:00
spatial . co2 . nodes ,
location = spatial . co2 . locations ,
2022-06-28 16:31:45 +00:00
carrier = " co2 stored " ,
unit = " t_co2 " ,
2021-07-01 18:09:04 +00:00
)
2023-02-16 16:21:58 +00:00
if options [ " regional_co2_sequestration_potential " ] [ " enable " ] :
upper_limit = (
options [ " regional_co2_sequestration_potential " ] [ " max_size " ] * 1e3
) # Mt
annualiser = options [ " regional_co2_sequestration_potential " ] [ " years_of_storage " ]
2023-01-24 17:44:39 +00:00
e_nom_max = pd . read_csv (
snakemake . input . sequestration_potential , index_col = 0
) . squeeze ( )
e_nom_max = (
e_nom_max . reindex ( spatial . co2 . locations )
. fillna ( 0.0 )
. clip ( upper = upper_limit )
. mul ( 1e6 )
/ annualiser
) # t
e_nom_max = e_nom_max . rename ( index = lambda x : x + " co2 stored " )
2023-02-16 19:13:26 +00:00
else :
e_nom_max = np . inf
2023-01-24 17:44:39 +00:00
2021-07-07 15:58:47 +00:00
n . madd (
" Store " ,
2021-07-09 10:50:40 +00:00
spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
e_nom_extendable = True ,
2023-01-24 17:44:39 +00:00
e_nom_max = e_nom_max ,
2021-07-01 18:09:04 +00:00
capital_cost = options [ " co2_sequestration_cost " ] ,
carrier = " co2 stored " ,
2021-07-09 10:50:40 +00:00
bus = spatial . co2 . nodes ,
2023-08-24 06:43:50 +00:00
lifetime = options [ " co2_sequestration_lifetime " ] ,
2021-07-01 18:09:04 +00:00
)
2019-04-17 12:24:22 +00:00
2023-02-16 17:42:19 +00:00
n . add ( " Carrier " , " co2 stored " )
2019-05-11 13:55:06 +00:00
if options [ " co2_vent " ] :
2021-07-07 15:58:47 +00:00
n . madd (
" Link " ,
2021-07-09 10:50:40 +00:00
spatial . co2 . vents ,
bus0 = spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
bus1 = " co2 atmosphere " ,
carrier = " co2 vent " ,
efficiency = 1.0 ,
p_nom_extendable = True ,
)
2019-04-17 12:24:22 +00:00
2021-07-07 15:58:47 +00:00
def add_co2_network ( n , costs ) :
2021-09-24 13:30:43 +00:00
logger . info ( " Adding CO2 network. " )
2021-07-09 08:42:16 +00:00
co2_links = create_network_topology ( n , " CO2 pipeline " )
2021-07-09 11:10:43 +00:00
cost_onshore = (
( 1 - co2_links . underwater_fraction )
* costs . at [ " CO2 pipeline " , " fixed " ]
* co2_links . length
2023-03-06 08:27:45 +00:00
)
2021-07-09 12:36:13 +00:00
cost_submarine = (
co2_links . underwater_fraction
* costs . at [ " CO2 submarine pipeline " , " fixed " ]
* co2_links . length
2023-03-06 08:27:45 +00:00
)
2021-07-09 11:10:43 +00:00
capital_cost = cost_onshore + cost_submarine
2021-09-29 12:37:36 +00:00
2021-07-07 15:58:47 +00:00
n . madd (
" Link " ,
co2_links . index ,
bus0 = co2_links . bus0 . values + " co2 stored " ,
bus1 = co2_links . bus1 . values + " co2 stored " ,
p_min_pu = - 1 ,
p_nom_extendable = True ,
length = co2_links . length . values ,
2021-07-09 11:10:43 +00:00
capital_cost = capital_cost . values ,
2021-07-07 15:58:47 +00:00
carrier = " CO2 pipeline " ,
lifetime = costs . at [ " CO2 pipeline " , " lifetime " ] ,
)
2023-02-08 21:57:01 +00:00
def add_allam ( n , costs ) :
2023-02-16 16:21:58 +00:00
logger . info ( " Adding Allam cycle gas power plants. " )
2023-03-06 08:27:45 +00:00
2023-02-08 21:57:01 +00:00
nodes = pop_layout . index
2023-03-06 08:27:45 +00:00
2023-02-08 21:57:01 +00:00
n . madd (
" Link " ,
2023-02-16 16:21:58 +00:00
nodes ,
suffix = " allam " ,
bus0 = spatial . gas . df . loc [ nodes , " nodes " ] . values ,
bus1 = nodes ,
bus2 = spatial . co2 . df . loc [ nodes , " nodes " ] . values ,
carrier = " allam " ,
p_nom_extendable = True ,
# TODO: add costs to technology-data
capital_cost = 0.6 * 1.5e6 * 0.1 , # efficiency * EUR/MW * annuity
marginal_cost = 2 ,
efficiency = 0.6 ,
efficiency2 = costs . at [ " gas " , " CO2 intensity " ] ,
lifetime = 30.0 ,
)
2023-02-08 21:57:01 +00:00
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
def add_dac ( n , costs ) :
heat_carriers = [ " urban central heat " , " services urban decentral heat " ]
heat_buses = n . buses . index [ n . buses . carrier . isin ( heat_carriers ) ]
locations = n . buses . location [ heat_buses ]
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
efficiency2 = - (
costs . at [ " direct air capture " , " electricity-input " ]
+ costs . at [ " direct air capture " , " compression-electricity-input " ]
)
efficiency3 = - (
costs . at [ " direct air capture " , " heat-input " ]
- costs . at [ " direct air capture " , " compression-heat-output " ]
)
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
2021-09-29 14:13:23 +00:00
heat_buses . str . replace ( " heat " , " DAC " ) ,
2021-07-01 18:09:04 +00:00
bus0 = " co2 atmosphere " ,
2021-07-09 10:50:40 +00:00
bus1 = spatial . co2 . df . loc [ locations , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
bus2 = locations . values ,
bus3 = heat_buses ,
carrier = " DAC " ,
capital_cost = costs . at [ " direct air capture " , " fixed " ] ,
efficiency = 1.0 ,
efficiency2 = efficiency2 ,
efficiency3 = efficiency3 ,
p_nom_extendable = True ,
lifetime = costs . at [ " direct air capture " , " lifetime " ] ,
)
2019-04-17 12:24:22 +00:00
2023-03-10 13:12:22 +00:00
def add_co2limit ( n , nyears = 1.0 , limit = 0.0 ) :
2021-12-03 16:22:06 +00:00
logger . info ( f " Adding CO2 budget limit as per unit of 1990 levels of { limit } " )
2019-04-17 12:24:22 +00:00
2023-06-15 16:52:25 +00:00
countries = snakemake . params . countries
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
sectors = emission_sectors_from_opts ( opts )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
# convert Mt to tCO2
co2_totals = 1e6 * pd . read_csv ( snakemake . input . co2_totals_name , index_col = 0 )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
co2_limit = co2_totals . loc [ countries , sectors ] . sum ( ) . sum ( )
2019-04-17 12:24:22 +00:00
2023-03-10 13:12:22 +00:00
co2_limit * = limit * nyears
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
n . add (
" GlobalConstraint " ,
" CO2Limit " ,
carrier_attribute = " co2_emissions " ,
sense = " <= " ,
constant = co2_limit ,
)
2019-04-17 12:24:22 +00:00
2023-03-06 08:27:45 +00:00
2021-07-01 18:09:04 +00:00
# TODO PyPSA-Eur merge issue
2019-04-17 12:24:22 +00:00
def average_every_nhours ( n , offset ) :
2021-07-01 18:09:04 +00:00
logger . info ( f " Resampling the network to { offset } " )
2019-04-17 12:24:22 +00:00
m = n . copy ( with_time = False )
snapshot_weightings = n . snapshot_weightings . resample ( offset ) . sum ( )
m . set_snapshots ( snapshot_weightings . index )
m . snapshot_weightings = snapshot_weightings
for c in n . iterate_components ( ) :
pnl = getattr ( m , c . list_name + " _t " )
2021-07-01 18:09:04 +00:00
for k , df in c . pnl . items ( ) :
2019-04-17 12:24:22 +00:00
if not df . empty :
if c . list_name == " stores " and k == " e_max_pu " :
pnl [ k ] = df . resample ( offset ) . min ( )
elif c . list_name == " stores " and k == " e_min_pu " :
pnl [ k ] = df . resample ( offset ) . max ( )
else :
pnl [ k ] = df . resample ( offset ) . mean ( )
return m
2021-07-01 18:09:04 +00:00
def cycling_shift ( df , steps = 1 ) :
"""
Cyclic shift on index of pd . Series | pd . DataFrame by number of steps .
"""
2019-04-17 12:24:22 +00:00
df = df . copy ( )
2021-07-01 18:09:04 +00:00
new_index = np . roll ( df . index , steps )
df . values [ : ] = df . reindex ( index = new_index ) . values
2019-04-17 12:24:22 +00:00
return df
2023-05-17 17:25:45 +00:00
def prepare_costs ( cost_file , params , nyears ) :
2019-04-17 12:24:22 +00:00
# set all asset costs and other parameters
2021-07-01 18:09:04 +00:00
costs = pd . read_csv ( cost_file , index_col = [ 0 , 1 ] ) . sort_index ( )
2019-04-17 12:24:22 +00:00
# correct units to MW and EUR
2021-07-01 18:09:04 +00:00
costs . loc [ costs . unit . str . contains ( " /kW " ) , " value " ] * = 1e3
2019-04-17 12:24:22 +00:00
2020-07-29 13:50:40 +00:00
# min_count=1 is important to generate NaNs which are then filled by fillna
costs = (
costs . loc [ : , " value " ] . unstack ( level = 1 ) . groupby ( " technology " ) . sum ( min_count = 1 )
2023-03-06 08:27:45 +00:00
)
2023-03-06 13:47:46 +00:00
2023-05-17 17:25:45 +00:00
costs = costs . fillna ( params [ " fill_values " ] )
2023-03-06 08:27:45 +00:00
2023-03-07 16:21:00 +00:00
def annuity_factor ( v ) :
2023-05-10 08:02:41 +00:00
return calculate_annuity ( v [ " lifetime " ] , v [ " discount rate " ] ) + v [ " FOM " ] / 100
2023-03-07 17:25:43 +00:00
2021-07-01 18:09:04 +00:00
costs [ " fixed " ] = [
2023-03-10 13:12:22 +00:00
annuity_factor ( v ) * v [ " investment " ] * nyears for i , v in costs . iterrows ( )
2021-07-01 18:09:04 +00:00
]
2019-04-17 12:24:22 +00:00
return costs
2021-07-01 18:09:04 +00:00
def add_generation ( n , costs ) :
2023-02-24 13:42:51 +00:00
logger . info ( " Adding electricity generation " )
2019-04-17 12:24:22 +00:00
2021-06-18 07:45:29 +00:00
nodes = pop_layout . index
2020-01-11 08:11:09 +00:00
2021-07-01 18:09:04 +00:00
fallback = { " OCGT " : " gas " }
conventionals = options . get ( " conventional_generation " , fallback )
for generator , carrier in conventionals . items ( ) :
2022-03-18 12:46:40 +00:00
carrier_nodes = vars ( spatial ) [ carrier ] . nodes
2021-07-02 13:00:19 +00:00
add_carrier_buses ( n , carrier , carrier_nodes )
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes + " " + generator ,
2021-07-02 13:00:19 +00:00
bus0 = carrier_nodes ,
2021-07-01 18:09:04 +00:00
bus1 = nodes ,
bus2 = " co2 atmosphere " ,
marginal_cost = costs . at [ generator , " efficiency " ]
* costs . at [ generator , " VOM " ] , # NB: VOM is per MWel
capital_cost = costs . at [ generator , " efficiency " ]
* costs . at [ generator , " fixed " ] , # NB: fixed cost is per MWel
p_nom_extendable = True ,
carrier = generator ,
efficiency = costs . at [ generator , " efficiency " ] ,
efficiency2 = costs . at [ carrier , " CO2 intensity " ] ,
lifetime = costs . at [ generator , " lifetime " ] ,
)
2020-01-11 08:11:09 +00:00
2021-07-01 18:09:04 +00:00
2022-06-10 12:44:36 +00:00
def add_ammonia ( n , costs ) :
2023-02-24 13:42:51 +00:00
logger . info ( " Adding ammonia carrier with synthesis, cracking and storage " )
2022-06-10 12:44:36 +00:00
nodes = pop_layout . index
2023-06-15 16:52:25 +00:00
cf_industry = snakemake . params . industry
2022-06-10 14:43:29 +00:00
2022-06-10 12:44:36 +00:00
n . add ( " Carrier " , " NH3 " )
n . madd (
2022-06-10 14:53:37 +00:00
" Bus " , spatial . ammonia . nodes , location = spatial . ammonia . locations , carrier = " NH3 "
2022-06-10 12:44:36 +00:00
)
n . madd (
" Link " ,
nodes ,
suffix = " Haber-Bosch " ,
bus0 = nodes ,
2022-06-10 14:53:37 +00:00
bus1 = spatial . ammonia . nodes ,
2022-06-10 12:44:36 +00:00
bus2 = nodes + " H2 " ,
p_nom_extendable = True ,
carrier = " Haber-Bosch " ,
2022-06-23 13:17:41 +00:00
efficiency = 1
/ (
cf_industry [ " MWh_elec_per_tNH3_electrolysis " ]
/ cf_industry [ " MWh_NH3_per_tNH3 " ]
) , # output: MW_NH3 per MW_elec
efficiency2 = - cf_industry [ " MWh_H2_per_tNH3_electrolysis " ]
/ cf_industry [ " MWh_elec_per_tNH3_electrolysis " ] , # input: MW_H2 per MW_elec
2023-02-16 19:13:26 +00:00
capital_cost = costs . at [ " Haber-Bosch " , " fixed " ] ,
lifetime = costs . at [ " Haber-Bosch " , " lifetime " ] ,
2022-06-10 12:44:36 +00:00
)
n . madd (
" Link " ,
nodes ,
suffix = " ammonia cracker " ,
2022-06-10 14:53:37 +00:00
bus0 = spatial . ammonia . nodes ,
2022-06-10 12:44:36 +00:00
bus1 = nodes + " H2 " ,
p_nom_extendable = True ,
2022-06-10 14:43:29 +00:00
carrier = " ammonia cracker " ,
2022-06-23 13:17:41 +00:00
efficiency = 1 / cf_industry [ " MWh_NH3_per_MWh_H2_cracker " ] ,
2022-06-10 14:43:29 +00:00
capital_cost = costs . at [ " Ammonia cracker " , " fixed " ]
/ cf_industry [ " MWh_NH3_per_MWh_H2_cracker " ] , # given per MW_H2
2022-06-10 12:44:36 +00:00
lifetime = costs . at [ " Ammonia cracker " , " lifetime " ] ,
)
# Ammonia Storage
n . madd (
" Store " ,
2022-06-10 14:53:37 +00:00
spatial . ammonia . nodes ,
2022-06-10 12:44:36 +00:00
suffix = " ammonia store " ,
2022-06-10 14:53:37 +00:00
bus = spatial . ammonia . nodes ,
2022-06-10 12:44:36 +00:00
e_nom_extendable = True ,
e_cyclic = True ,
carrier = " ammonia store " ,
capital_cost = costs . at [ " NH3 (l) storage tank incl. liquefaction " , " fixed " ] ,
lifetime = costs . at [ " NH3 (l) storage tank incl. liquefaction " , " lifetime " ] ,
)
2021-07-01 18:09:04 +00:00
def add_wave ( n , wave_cost_factor ) :
# TODO: handle in Snakefile
wave_fn = " data/WindWaveWEC_GLTB.xlsx "
2020-01-11 08:11:09 +00:00
# in kW
2021-07-01 18:09:04 +00:00
capacity = pd . Series ( { " Attenuator " : 750 , " F2HB " : 1000 , " MultiPA " : 600 } )
2020-01-11 08:11:09 +00:00
# in EUR/MW
2023-05-10 08:02:41 +00:00
annuity_factor = calculate_annuity ( 25 , 0.07 ) + 0.03
2021-07-01 18:09:04 +00:00
costs = (
1e6
* wave_cost_factor
* annuity_factor
* pd . Series ( { " Attenuator " : 2.5 , " F2HB " : 2 , " MultiPA " : 1.5 } )
)
2020-01-11 08:11:09 +00:00
2021-07-01 18:09:04 +00:00
sheets = pd . read_excel (
wave_fn ,
sheet_name = [ " FirthForth " , " Hebrides " ] ,
usecols = [ " Attenuator " , " F2HB " , " MultiPA " ] ,
index_col = 0 ,
skiprows = [ 0 ] ,
parse_dates = True ,
)
2020-01-11 08:11:09 +00:00
2021-07-01 18:09:04 +00:00
wave = pd . concat (
[ sheets [ l ] . divide ( capacity , axis = 1 ) for l in locations ] , keys = locations , axis = 1
2020-01-11 08:11:09 +00:00
)
for wave_type in costs . index :
n . add (
" Generator " ,
2021-07-01 18:09:04 +00:00
" Hebrides " + wave_type ,
bus = " GB4 0 " , # TODO this location is hardcoded
p_nom_extendable = True ,
carrier = " wave " ,
capital_cost = costs [ wave_type ] ,
p_max_pu = wave [ " Hebrides " , wave_type ] ,
)
2019-04-17 12:24:22 +00:00
2020-03-25 11:52:25 +00:00
2021-07-01 18:09:04 +00:00
def insert_electricity_distribution_grid ( n , costs ) :
# TODO pop_layout?
# TODO options?
2020-03-25 11:52:25 +00:00
2023-02-16 17:42:19 +00:00
cost_factor = options [ " electricity_distribution_grid_cost_factor " ]
logger . info (
f " Inserting electricity distribution grid with investment cost factor of { cost_factor : .2f } "
)
2020-03-25 11:52:25 +00:00
nodes = pop_layout . index
2021-07-01 18:09:04 +00:00
n . madd (
" Bus " ,
nodes + " low voltage " ,
location = nodes ,
2022-06-28 16:31:45 +00:00
carrier = " low voltage " ,
unit = " MWh_el " ,
2021-07-01 18:09:04 +00:00
)
2020-03-25 11:52:25 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes + " electricity distribution grid " ,
bus0 = nodes ,
bus1 = nodes + " low voltage " ,
p_nom_extendable = True ,
p_min_pu = - 1 ,
carrier = " electricity distribution grid " ,
efficiency = 1 ,
lifetime = costs . at [ " electricity distribution grid " , " lifetime " ] ,
capital_cost = costs . at [ " electricity distribution grid " , " fixed " ] * cost_factor ,
)
2021-07-06 16:32:35 +00:00
# this catches regular electricity load and "industry electricity" and
# "agriculture machinery electric" and "agriculture electricity"
loads = n . loads . index [ n . loads . carrier . str . contains ( " electric " ) ]
2021-07-01 18:09:04 +00:00
n . loads . loc [ loads , " bus " ] + = " low voltage "
bevs = n . links . index [ n . links . carrier == " BEV charger " ]
n . links . loc [ bevs , " bus0 " ] + = " low voltage "
v2gs = n . links . index [ n . links . carrier == " V2G " ]
n . links . loc [ v2gs , " bus1 " ] + = " low voltage "
hps = n . links . index [ n . links . carrier . str . contains ( " heat pump " ) ]
n . links . loc [ hps , " bus0 " ] + = " low voltage "
rh = n . links . index [ n . links . carrier . str . contains ( " resistive heater " ) ]
n . links . loc [ rh , " bus0 " ] + = " low voltage "
mchp = n . links . index [ n . links . carrier . str . contains ( " micro gas " ) ]
n . links . loc [ mchp , " bus1 " ] + = " low voltage "
# set existing solar to cost of utility cost rather the 50-50 rooftop-utility
solar = n . generators . index [ n . generators . carrier == " solar " ]
n . generators . loc [ solar , " capital_cost " ] = costs . at [ " solar-utility " , " fixed " ]
if snakemake . wildcards . clusters [ - 1 : ] == " m " :
simplified_pop_layout = pd . read_csv (
snakemake . input . simplified_pop_layout , index_col = 0
)
pop_solar = simplified_pop_layout . total . rename ( index = lambda x : x + " solar " )
else :
pop_solar = pop_layout . total . rename ( index = lambda x : x + " solar " )
2020-09-22 18:18:21 +00:00
2021-07-01 18:09:04 +00:00
# add max solar rooftop potential assuming 0.1 kW/m2 and 10 m2/person,
# i.e. 1 kW/person (population data is in thousands of people) so we get MW
potential = 0.1 * 10 * pop_solar
n . madd (
" Generator " ,
solar ,
suffix = " rooftop " ,
bus = n . generators . loc [ solar , " bus " ] + " low voltage " ,
carrier = " solar rooftop " ,
p_nom_extendable = True ,
p_nom_max = potential ,
marginal_cost = n . generators . loc [ solar , " marginal_cost " ] ,
capital_cost = costs . at [ " solar-rooftop " , " fixed " ] ,
efficiency = n . generators . loc [ solar , " efficiency " ] ,
2021-08-24 16:58:42 +00:00
p_max_pu = n . generators_t . p_max_pu [ solar ] ,
2021-09-27 09:16:12 +00:00
lifetime = costs . at [ " solar-rooftop " , " lifetime " ] ,
2021-07-01 18:09:04 +00:00
)
n . add ( " Carrier " , " home battery " )
2020-03-25 11:52:25 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Bus " ,
nodes + " home battery " ,
location = nodes ,
2022-06-28 16:31:45 +00:00
carrier = " home battery " ,
unit = " MWh_el " ,
2021-07-01 18:09:04 +00:00
)
2020-03-25 11:52:25 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Store " ,
nodes + " home battery " ,
bus = nodes + " home battery " ,
e_cyclic = True ,
e_nom_extendable = True ,
carrier = " home battery " ,
2021-07-01 18:16:12 +00:00
capital_cost = costs . at [ " home battery storage " , " fixed " ] ,
2021-07-01 18:09:04 +00:00
lifetime = costs . at [ " battery storage " , " lifetime " ] ,
)
2020-03-25 11:52:25 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes + " home battery charger " ,
bus0 = nodes + " low voltage " ,
bus1 = nodes + " home battery " ,
carrier = " home battery charger " ,
efficiency = costs . at [ " battery inverter " , " efficiency " ] * * 0.5 ,
2021-07-01 18:16:12 +00:00
capital_cost = costs . at [ " home battery inverter " , " fixed " ] ,
2021-07-01 18:09:04 +00:00
p_nom_extendable = True ,
lifetime = costs . at [ " battery inverter " , " lifetime " ] ,
)
2020-03-26 09:06:59 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes + " home battery discharger " ,
bus0 = nodes + " home battery " ,
bus1 = nodes + " low voltage " ,
carrier = " home battery discharger " ,
efficiency = costs . at [ " battery inverter " , " efficiency " ] * * 0.5 ,
marginal_cost = options [ " marginal_cost_storage " ] ,
p_nom_extendable = True ,
lifetime = costs . at [ " battery inverter " , " lifetime " ] ,
)
2020-09-15 15:44:27 +00:00
2021-07-01 18:09:04 +00:00
def insert_gas_distribution_costs ( n , costs ) :
# TODO options?
2020-09-25 13:25:41 +00:00
2020-09-15 16:03:33 +00:00
f_costs = options [ " gas_distribution_grid_cost_factor " ]
2021-07-08 12:41:34 +00:00
2023-02-16 17:42:19 +00:00
logger . info (
f " Inserting gas distribution grid with investment cost factor of { f_costs } "
)
2021-07-01 18:09:04 +00:00
capital_cost = costs . loc [ " electricity distribution grid " ] [ " fixed " ] * f_costs
2020-09-15 16:03:33 +00:00
# gas boilers
2021-07-01 18:09:04 +00:00
gas_b = n . links . index [
n . links . carrier . str . contains ( " gas boiler " )
& ( ~ n . links . carrier . str . contains ( " urban central " ) )
]
n . links . loc [ gas_b , " capital_cost " ] + = capital_cost
2021-07-08 12:41:34 +00:00
2020-09-15 16:03:33 +00:00
# micro CHPs
2021-07-01 18:09:04 +00:00
mchp = n . links . index [ n . links . carrier . str . contains ( " micro gas " ) ]
n . links . loc [ mchp , " capital_cost " ] + = capital_cost
2020-09-15 16:03:33 +00:00
2021-07-01 18:09:04 +00:00
def add_electricity_grid_connection ( n , costs ) :
carriers = [ " onwind " , " solar " ]
2020-05-11 11:37:10 +00:00
2021-07-01 18:09:04 +00:00
gens = n . generators . index [ n . generators . carrier . isin ( carriers ) ]
2020-03-26 09:06:59 +00:00
2021-07-01 18:09:04 +00:00
n . generators . loc [ gens , " capital_cost " ] + = costs . at [
" electricity grid connection " , " fixed "
]
2020-05-11 11:37:10 +00:00
2020-03-25 11:52:25 +00:00
2021-11-03 19:34:43 +00:00
def add_storage_and_grids ( n , costs ) :
2021-12-03 16:22:06 +00:00
logger . info ( " Add hydrogen storage " )
2021-06-18 07:45:29 +00:00
2019-04-17 12:24:22 +00:00
nodes = pop_layout . index
2021-07-02 13:08:06 +00:00
n . add ( " Carrier " , " H2 " )
2021-06-18 07:45:29 +00:00
2021-07-01 18:09:04 +00:00
n . madd ( " Bus " , nodes + " H2 " , location = nodes , carrier = " H2 " , unit = " MWh_LHV " )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes + " H2 Electrolysis " ,
bus1 = nodes + " H2 " ,
bus0 = nodes ,
p_nom_extendable = True ,
carrier = " H2 Electrolysis " ,
efficiency = costs . at [ " electrolysis " , " efficiency " ] ,
capital_cost = costs . at [ " electrolysis " , " fixed " ] ,
lifetime = costs . at [ " electrolysis " , " lifetime " ] ,
)
2020-09-22 07:52:53 +00:00
2023-04-22 07:44:13 +00:00
if options [ " hydrogen_fuel_cell " ] :
logger . info ( " Adding hydrogen fuel cell for re-electrification. " )
n . madd (
" Link " ,
nodes + " H2 Fuel Cell " ,
bus0 = nodes + " H2 " ,
bus1 = nodes ,
p_nom_extendable = True ,
carrier = " H2 Fuel Cell " ,
efficiency = costs . at [ " fuel cell " , " efficiency " ] ,
capital_cost = costs . at [ " fuel cell " , " fixed " ]
* costs . at [ " fuel cell " , " efficiency " ] , # NB: fixed cost is per MWel
lifetime = costs . at [ " fuel cell " , " lifetime " ] ,
)
if options [ " hydrogen_turbine " ] :
2023-04-30 08:44:05 +00:00
logger . info (
" Adding hydrogen turbine for re-electrification. Assuming OCGT technology costs. "
)
2023-04-30 08:43:49 +00:00
# TODO: perhaps replace with hydrogen-specific technology assumptions.
2023-04-22 07:44:13 +00:00
n . madd (
" Link " ,
nodes + " H2 turbine " ,
bus0 = nodes + " H2 " ,
bus1 = nodes ,
p_nom_extendable = True ,
carrier = " H2 turbine " ,
efficiency = costs . at [ " OCGT " , " efficiency " ] ,
capital_cost = costs . at [ " OCGT " , " fixed " ]
* costs . at [ " OCGT " , " efficiency " ] , # NB: fixed cost is per MWel
lifetime = costs . at [ " OCGT " , " lifetime " ] ,
)
2021-07-01 18:09:04 +00:00
2023-06-15 16:52:25 +00:00
cavern_types = snakemake . params . sector [ " hydrogen_underground_storage_locations " ]
2022-04-11 15:08:25 +00:00
h2_caverns = pd . read_csv ( snakemake . input . h2_cavern , index_col = 0 )
2022-04-12 08:45:11 +00:00
2023-05-24 09:13:37 +00:00
if (
not h2_caverns . empty
and options [ " hydrogen_underground_storage " ]
and set ( cavern_types ) . intersection ( h2_caverns . columns )
) :
2022-04-11 15:08:25 +00:00
h2_caverns = h2_caverns [ cavern_types ] . sum ( axis = 1 )
2021-11-29 08:12:07 +00:00
2022-04-11 15:08:25 +00:00
# only use sites with at least 2 TWh potential
h2_caverns = h2_caverns [ h2_caverns > 2 ]
2021-11-29 08:12:07 +00:00
2022-04-11 15:08:25 +00:00
# convert TWh to MWh
h2_caverns = h2_caverns * 1e6
2021-11-29 08:12:07 +00:00
2022-04-11 15:08:25 +00:00
# clip at 1000 TWh for one location
h2_caverns . clip ( upper = 1e9 , inplace = True )
2021-11-29 08:12:07 +00:00
2021-12-03 16:22:06 +00:00
logger . info ( " Add hydrogen underground storage " )
2021-11-29 08:12:07 +00:00
h2_capital_cost = costs . at [ " hydrogen storage underground " , " fixed " ]
n . madd (
" Store " ,
h2_caverns . index + " H2 Store " ,
bus = h2_caverns . index + " H2 " ,
2021-07-01 18:09:04 +00:00
e_nom_extendable = True ,
2021-11-29 08:12:07 +00:00
e_nom_max = h2_caverns . values ,
2021-07-01 18:09:04 +00:00
e_cyclic = True ,
carrier = " H2 Store " ,
2022-03-18 12:46:40 +00:00
capital_cost = h2_capital_cost ,
lifetime = costs . at [ " hydrogen storage underground " , " lifetime " ] ,
2021-07-01 18:09:04 +00:00
)
2020-09-22 07:52:53 +00:00
2021-07-01 18:09:04 +00:00
# hydrogen stored overground (where not already underground)
2023-02-10 06:49:45 +00:00
h2_capital_cost = costs . at [
" hydrogen storage tank type 1 including compressor " , " fixed "
]
2021-11-29 08:12:07 +00:00
nodes_overground = h2_caverns . index . symmetric_difference ( nodes )
2019-05-13 14:47:54 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Store " ,
nodes_overground + " H2 Store " ,
bus = nodes_overground + " H2 " ,
e_nom_extendable = True ,
e_cyclic = True ,
carrier = " H2 Store " ,
capital_cost = h2_capital_cost ,
)
2019-04-17 12:24:22 +00:00
2021-11-29 11:42:10 +00:00
if options [ " gas_network " ] or options [ " H2_retrofit " ] :
2021-11-03 19:34:43 +00:00
fn = snakemake . input . clustered_gas_network
2021-11-04 20:48:54 +00:00
gas_pipes = pd . read_csv ( fn , index_col = 0 )
2021-06-21 10:36:06 +00:00
2021-11-29 11:42:10 +00:00
if options [ " gas_network " ] :
2021-12-03 16:22:06 +00:00
logger . info (
" Add natural gas infrastructure, incl. LNG terminals, production and entry-points. "
)
2021-11-29 11:42:10 +00:00
2021-08-04 09:09:31 +00:00
if options [ " H2_retrofit " ] :
2021-11-04 20:48:54 +00:00
gas_pipes [ " p_nom_max " ] = gas_pipes . p_nom
2021-08-04 09:09:31 +00:00
gas_pipes [ " p_nom_min " ] = 0.0
2021-11-29 11:42:10 +00:00
# 0.1 EUR/MWkm/a to prefer decommissioning to address degeneracy
gas_pipes [ " capital_cost " ] = 0.1 * gas_pipes . length
2021-08-04 09:09:31 +00:00
else :
gas_pipes [ " p_nom_max " ] = np . inf
2021-11-04 20:48:54 +00:00
gas_pipes [ " p_nom_min " ] = gas_pipes . p_nom
2021-11-03 19:34:43 +00:00
gas_pipes [ " capital_cost " ] = (
gas_pipes . length * costs . at [ " CH4 (g) pipeline " , " fixed " ]
2023-03-06 08:27:45 +00:00
)
2021-08-04 09:09:31 +00:00
2021-07-02 13:00:19 +00:00
n . madd (
" Link " ,
gas_pipes . index ,
bus0 = gas_pipes . bus0 + " gas " ,
bus1 = gas_pipes . bus1 + " gas " ,
p_min_pu = gas_pipes . p_min_pu ,
2021-11-03 19:34:43 +00:00
p_nom = gas_pipes . p_nom ,
2021-08-04 09:09:31 +00:00
p_nom_extendable = True ,
p_nom_max = gas_pipes . p_nom_max ,
p_nom_min = gas_pipes . p_nom_min ,
2021-11-03 19:34:43 +00:00
length = gas_pipes . length ,
2021-08-04 09:09:31 +00:00
capital_cost = gas_pipes . capital_cost ,
2021-11-13 15:48:08 +00:00
tags = gas_pipes . name ,
2021-11-03 19:34:43 +00:00
carrier = " gas pipeline " ,
2021-11-04 20:48:54 +00:00
lifetime = costs . at [ " CH4 (g) pipeline " , " lifetime " ] ,
2021-07-02 13:00:19 +00:00
)
2022-03-18 09:18:24 +00:00
2021-11-03 19:34:43 +00:00
# remove fossil generators where there is neither
# production, LNG terminal, nor entry-point beyond system scope
2021-11-04 20:48:54 +00:00
2021-11-12 11:50:46 +00:00
fn = snakemake . input . gas_input_nodes_simplified
2021-11-10 17:24:13 +00:00
gas_input_nodes = pd . read_csv ( fn , index_col = 0 )
unique = gas_input_nodes . index . unique ( )
gas_i = n . generators . carrier == " gas "
internal_i = ~ n . generators . bus . map ( n . buses . location ) . isin ( unique )
remove_i = n . generators [ gas_i & internal_i ] . index
2021-07-02 13:00:19 +00:00
n . generators . drop ( remove_i , inplace = True )
2021-06-21 10:36:06 +00:00
2021-11-10 17:24:13 +00:00
p_nom = gas_input_nodes . sum ( axis = 1 ) . rename ( lambda x : x + " gas " )
n . generators . loc [ gas_i , " p_nom_extendable " ] = False
n . generators . loc [ gas_i , " p_nom " ] = p_nom
2021-11-04 20:48:54 +00:00
# add candidates for new gas pipelines to achieve full connectivity
2021-11-03 19:34:43 +00:00
2021-11-04 20:48:54 +00:00
G = nx . Graph ( )
gas_buses = n . buses . loc [ n . buses . carrier == " gas " , " location " ]
G . add_nodes_from ( np . unique ( gas_buses . values ) )
sel = gas_pipes . p_nom > 1500
attrs = [ " bus0 " , " bus1 " , " length " ]
G . add_weighted_edges_from ( gas_pipes . loc [ sel , attrs ] . values )
# find all complement edges
complement_edges = pd . DataFrame ( complement ( G ) . edges , columns = [ " bus0 " , " bus1 " ] )
complement_edges [ " length " ] = complement_edges . apply ( haversine , axis = 1 )
# apply k_edge_augmentation weighted by length of complement edges
k_edge = options . get ( " gas_network_connectivity_upgrade " , 3 )
2023-10-08 09:20:36 +00:00
if augmentation := list (
2022-04-12 08:45:11 +00:00
k_edge_augmentation ( G , k_edge , avail = complement_edges . values )
2023-10-08 09:20:36 +00:00
) :
2022-04-11 15:08:25 +00:00
new_gas_pipes = pd . DataFrame ( augmentation , columns = [ " bus0 " , " bus1 " ] )
new_gas_pipes [ " length " ] = new_gas_pipes . apply ( haversine , axis = 1 )
new_gas_pipes . index = new_gas_pipes . apply (
lambda x : f " gas pipeline new { x . bus0 } <-> { x . bus1 } " , axis = 1
)
n . madd (
" Link " ,
new_gas_pipes . index ,
bus0 = new_gas_pipes . bus0 + " gas " ,
bus1 = new_gas_pipes . bus1 + " gas " ,
p_min_pu = - 1 , # new gas pipes are bidirectional
p_nom_extendable = True ,
length = new_gas_pipes . length ,
capital_cost = new_gas_pipes . length
* costs . at [ " CH4 (g) pipeline " , " fixed " ] ,
carrier = " gas pipeline new " ,
lifetime = costs . at [ " CH4 (g) pipeline " , " lifetime " ] ,
)
2021-11-03 19:34:43 +00:00
2021-11-29 11:42:10 +00:00
if options [ " H2_retrofit " ] :
logger . info ( " Add retrofitting options of existing CH4 pipes to H2 pipes. " )
2021-08-04 07:48:23 +00:00
2021-11-29 11:42:10 +00:00
fr = " gas pipeline "
to = " H2 pipeline retrofitted "
h2_pipes = gas_pipes . rename ( index = lambda x : x . replace ( fr , to ) )
2021-08-04 07:48:23 +00:00
n . madd (
" Link " ,
h2_pipes . index ,
bus0 = h2_pipes . bus0 + " H2 " ,
bus1 = h2_pipes . bus1 + " H2 " ,
2021-11-04 20:48:54 +00:00
p_min_pu = - 1.0 , # allow that all H2 retrofit pipelines can be used in both directions
p_nom_max = h2_pipes . p_nom * options [ " H2_retrofit_capacity_per_CH4 " ] ,
2021-08-04 07:48:23 +00:00
p_nom_extendable = True ,
2021-11-04 20:48:54 +00:00
length = h2_pipes . length ,
capital_cost = costs . at [ " H2 (g) pipeline repurposed " , " fixed " ]
* h2_pipes . length ,
2021-11-13 15:48:08 +00:00
tags = h2_pipes . name ,
2021-08-04 07:48:23 +00:00
carrier = " H2 pipeline retrofitted " ,
2021-11-04 20:48:54 +00:00
lifetime = costs . at [ " H2 (g) pipeline repurposed " , " lifetime " ] ,
2021-08-04 07:48:23 +00:00
)
2019-04-17 12:24:22 +00:00
2021-11-04 20:48:54 +00:00
if options . get ( " H2_network " , True ) :
2021-11-29 11:42:10 +00:00
logger . info ( " Add options for new hydrogen pipelines. " )
2021-11-04 20:48:54 +00:00
h2_pipes = create_network_topology (
n , " H2 pipeline " , carriers = [ " DC " , " gas pipeline " ]
)
2021-11-03 19:34:43 +00:00
2021-11-04 20:48:54 +00:00
# TODO Add efficiency losses
n . madd (
" Link " ,
h2_pipes . index ,
bus0 = h2_pipes . bus0 . values + " H2 " ,
bus1 = h2_pipes . bus1 . values + " H2 " ,
p_min_pu = - 1 ,
p_nom_extendable = True ,
length = h2_pipes . length . values ,
capital_cost = costs . at [ " H2 (g) pipeline " , " fixed " ] * h2_pipes . length . values ,
carrier = " H2 pipeline " ,
lifetime = costs . at [ " H2 (g) pipeline " , " lifetime " ] ,
)
2021-11-03 19:34:43 +00:00
2021-07-01 18:09:04 +00:00
n . add ( " Carrier " , " battery " )
n . madd ( " Bus " , nodes + " battery " , location = nodes , carrier = " battery " , unit = " MWh_el " )
n . madd (
" Store " ,
nodes + " battery " ,
bus = nodes + " battery " ,
e_cyclic = True ,
e_nom_extendable = True ,
carrier = " battery " ,
capital_cost = costs . at [ " battery storage " , " fixed " ] ,
lifetime = costs . at [ " battery storage " , " lifetime " ] ,
)
n . madd (
" Link " ,
nodes + " battery charger " ,
bus0 = nodes ,
bus1 = nodes + " battery " ,
carrier = " battery charger " ,
efficiency = costs . at [ " battery inverter " , " efficiency " ] * * 0.5 ,
capital_cost = costs . at [ " battery inverter " , " fixed " ] ,
p_nom_extendable = True ,
lifetime = costs . at [ " battery inverter " , " lifetime " ] ,
)
n . madd (
" Link " ,
nodes + " battery discharger " ,
bus0 = nodes + " battery " ,
bus1 = nodes ,
carrier = " battery discharger " ,
efficiency = costs . at [ " battery inverter " , " efficiency " ] * * 0.5 ,
marginal_cost = options [ " marginal_cost_storage " ] ,
p_nom_extendable = True ,
lifetime = costs . at [ " battery inverter " , " lifetime " ] ,
)
2019-04-17 12:24:22 +00:00
if options [ " methanation " ] :
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
2021-07-09 10:50:40 +00:00
spatial . nodes ,
suffix = " Sabatier " ,
2021-07-01 18:09:04 +00:00
bus0 = nodes + " H2 " ,
2021-08-04 12:47:13 +00:00
bus1 = spatial . gas . nodes ,
2021-07-09 10:50:40 +00:00
bus2 = spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
p_nom_extendable = True ,
carrier = " Sabatier " ,
efficiency = costs . at [ " methanation " , " efficiency " ] ,
efficiency2 = - costs . at [ " methanation " , " efficiency " ]
* costs . at [ " gas " , " CO2 intensity " ] ,
2021-08-04 16:19:42 +00:00
capital_cost = costs . at [ " methanation " , " fixed " ]
* costs . at [ " methanation " , " efficiency " ] , # costs given per kW_gas
2021-07-01 18:09:04 +00:00
lifetime = costs . at [ " methanation " , " lifetime " ] ,
)
2019-04-17 12:24:22 +00:00
if options [ " helmeth " ] :
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
2021-07-09 10:50:40 +00:00
spatial . nodes ,
suffix = " helmeth " ,
2021-07-01 18:09:04 +00:00
bus0 = nodes ,
2021-08-04 12:47:13 +00:00
bus1 = spatial . gas . nodes ,
2021-07-09 10:50:40 +00:00
bus2 = spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
carrier = " helmeth " ,
p_nom_extendable = True ,
efficiency = costs . at [ " helmeth " , " efficiency " ] ,
efficiency2 = - costs . at [ " helmeth " , " efficiency " ]
* costs . at [ " gas " , " CO2 intensity " ] ,
capital_cost = costs . at [ " helmeth " , " fixed " ] ,
lifetime = costs . at [ " helmeth " , " lifetime " ] ,
)
2019-04-17 12:24:22 +00:00
2022-06-27 16:06:59 +00:00
if options . get ( " coal_cc " ) :
2022-06-28 11:35:44 +00:00
n . madd (
" Link " ,
spatial . nodes ,
suffix = " coal CC " ,
bus0 = spatial . coal . nodes ,
bus1 = spatial . nodes ,
bus2 = " co2 atmosphere " ,
2023-01-24 17:44:39 +00:00
bus3 = spatial . co2 . nodes ,
2022-06-28 11:35:44 +00:00
marginal_cost = costs . at [ " coal " , " efficiency " ]
* costs . at [ " coal " , " VOM " ] , # NB: VOM is per MWel
capital_cost = costs . at [ " coal " , " efficiency " ] * costs . at [ " coal " , " fixed " ]
+ costs . at [ " biomass CHP capture " , " fixed " ]
* costs . at [ " coal " , " CO2 intensity " ] , # NB: fixed cost is per MWel
p_nom_extendable = True ,
carrier = " coal " ,
efficiency = costs . at [ " coal " , " efficiency " ] ,
efficiency2 = costs . at [ " coal " , " CO2 intensity " ]
* ( 1 - costs . at [ " biomass CHP capture " , " capture_rate " ] ) ,
efficiency3 = costs . at [ " coal " , " CO2 intensity " ]
* costs . at [ " biomass CHP capture " , " capture_rate " ] ,
lifetime = costs . at [ " coal " , " lifetime " ] ,
)
2019-04-17 12:24:22 +00:00
2023-10-03 14:42:48 +00:00
if options [ " SMR_cc " ] :
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
2021-07-09 10:50:40 +00:00
spatial . nodes ,
suffix = " SMR CC " ,
2021-08-04 12:47:13 +00:00
bus0 = spatial . gas . nodes ,
2021-07-01 18:09:04 +00:00
bus1 = nodes + " H2 " ,
bus2 = " co2 atmosphere " ,
2021-07-09 10:50:40 +00:00
bus3 = spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
p_nom_extendable = True ,
carrier = " SMR CC " ,
efficiency = costs . at [ " SMR CC " , " efficiency " ] ,
efficiency2 = costs . at [ " gas " , " CO2 intensity " ] * ( 1 - options [ " cc_fraction " ] ) ,
efficiency3 = costs . at [ " gas " , " CO2 intensity " ] * options [ " cc_fraction " ] ,
capital_cost = costs . at [ " SMR CC " , " fixed " ] ,
lifetime = costs . at [ " SMR CC " , " lifetime " ] ,
)
2023-10-03 14:42:48 +00:00
if options [ " SMR " ] :
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes + " SMR " ,
2021-08-04 12:47:13 +00:00
bus0 = spatial . gas . nodes ,
2021-07-01 18:09:04 +00:00
bus1 = nodes + " H2 " ,
bus2 = " co2 atmosphere " ,
p_nom_extendable = True ,
carrier = " SMR " ,
efficiency = costs . at [ " SMR " , " efficiency " ] ,
efficiency2 = costs . at [ " gas " , " CO2 intensity " ] ,
capital_cost = costs . at [ " SMR " , " fixed " ] ,
lifetime = costs . at [ " SMR " , " lifetime " ] ,
)
def add_land_transport ( n , costs ) :
# TODO options?
2020-11-30 15:20:26 +00:00
2021-12-03 16:22:06 +00:00
logger . info ( " Add land transport " )
2023-03-10 14:58:53 +00:00
nhours = n . snapshot_weightings . generators . sum ( )
2020-11-30 15:20:26 +00:00
2022-04-03 16:49:35 +00:00
transport = pd . read_csv (
snakemake . input . transport_demand , index_col = 0 , parse_dates = True
)
number_cars = pd . read_csv ( snakemake . input . transport_data , index_col = 0 ) [
" number cars "
]
avail_profile = pd . read_csv (
snakemake . input . avail_profile , index_col = 0 , parse_dates = True
)
dsm_profile = pd . read_csv (
snakemake . input . dsm_profile , index_col = 0 , parse_dates = True
)
2021-07-01 18:09:04 +00:00
fuel_cell_share = get ( options [ " land_transport_fuel_cell_share " ] , investment_year )
electric_share = get ( options [ " land_transport_electric_share " ] , investment_year )
2022-12-29 10:46:57 +00:00
ice_share = get ( options [ " land_transport_ice_share " ] , investment_year )
2023-03-06 08:27:45 +00:00
2022-12-29 10:46:57 +00:00
total_share = fuel_cell_share + electric_share + ice_share
if total_share != 1 :
2023-02-16 17:42:19 +00:00
logger . warning (
f " Total land transport shares sum up to { total_share : .2% } , corresponding to increased or decreased demand assumptions. "
)
2020-11-30 15:20:26 +00:00
2022-12-29 10:46:57 +00:00
logger . info ( f " FCEV share: { fuel_cell_share * 100 } % " )
logger . info ( f " EV share: { electric_share * 100 } % " )
logger . info ( f " ICEV share: { ice_share * 100 } % " )
2020-11-30 15:20:26 +00:00
2019-04-17 12:24:22 +00:00
nodes = pop_layout . index
2020-11-30 15:20:26 +00:00
if electric_share > 0 :
2021-07-01 18:09:04 +00:00
n . add ( " Carrier " , " Li ion " )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Bus " ,
nodes ,
suffix = " EV battery " ,
2023-10-24 12:06:17 +00:00
location = nodes ,
2022-06-28 16:31:45 +00:00
carrier = " Li ion " ,
unit = " MWh_el " ,
2021-07-01 18:09:04 +00:00
)
2021-07-08 12:41:34 +00:00
2021-07-01 18:09:04 +00:00
p_set = (
electric_share
* (
transport [ nodes ]
+ cycling_shift ( transport [ nodes ] , 1 )
+ cycling_shift ( transport [ nodes ] , 2 )
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
/ 3
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
n . madd (
" Load " ,
nodes ,
suffix = " land transport EV " ,
bus = nodes + " EV battery " ,
carrier = " land transport EV " ,
p_set = p_set ,
)
2019-04-17 12:24:22 +00:00
2022-04-03 16:49:35 +00:00
p_nom = number_cars * options . get ( " bev_charge_rate " , 0.011 ) * electric_share
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes ,
suffix = " BEV charger " ,
bus0 = nodes ,
bus1 = nodes + " EV battery " ,
p_nom = p_nom ,
carrier = " BEV charger " ,
p_max_pu = avail_profile [ nodes ] ,
efficiency = options . get ( " bev_charge_efficiency " , 0.9 ) ,
# These were set non-zero to find LU infeasibility when availability = 0.25
# p_nom_extendable=True,
# p_nom_min=p_nom,
# capital_cost=1e6, #i.e. so high it only gets built where necessary
)
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
if electric_share > 0 and options [ " v2g " ] :
n . madd (
" Link " ,
nodes ,
suffix = " V2G " ,
bus1 = nodes ,
bus0 = nodes + " EV battery " ,
p_nom = p_nom ,
carrier = " V2G " ,
p_max_pu = avail_profile [ nodes ] ,
efficiency = options . get ( " bev_charge_efficiency " , 0.9 ) ,
)
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
if electric_share > 0 and options [ " bev_dsm " ] :
2023-03-06 08:27:45 +00:00
e_nom = (
2022-04-03 16:49:35 +00:00
number_cars
* options . get ( " bev_energy " , 0.05 )
* options [ " bev_availability " ]
* electric_share
2023-03-06 08:27:45 +00:00
)
2020-11-30 15:20:26 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Store " ,
nodes ,
suffix = " battery storage " ,
bus = nodes + " EV battery " ,
carrier = " battery storage " ,
e_cyclic = True ,
e_nom = e_nom ,
e_max_pu = 1 ,
e_min_pu = dsm_profile [ nodes ] ,
)
2019-04-17 12:24:22 +00:00
2020-11-30 15:20:26 +00:00
if fuel_cell_share > 0 :
2021-07-01 18:09:04 +00:00
n . madd (
" Load " ,
nodes ,
suffix = " land transport fuel cell " ,
bus = nodes + " H2 " ,
carrier = " land transport fuel cell " ,
p_set = fuel_cell_share
/ options [ " transport_fuel_cell_efficiency " ]
* transport [ nodes ] ,
)
2020-11-30 15:20:26 +00:00
2021-01-26 09:55:38 +00:00
if ice_share > 0 :
2022-03-18 12:46:40 +00:00
if " oil " not in n . buses . carrier . unique ( ) :
n . madd (
" Bus " ,
2022-03-21 08:14:15 +00:00
spatial . oil . nodes ,
location = spatial . oil . locations ,
2022-06-28 16:31:45 +00:00
carrier = " oil " ,
2022-06-30 15:37:40 +00:00
unit = " MWh_LHV " ,
2021-07-01 18:09:04 +00:00
)
ice_efficiency = options [ " transport_internal_combustion_efficiency " ]
n . madd (
2023-10-24 12:06:17 +00:00
" Bus " ,
spatial . oil . land_transport ,
location = nodes ,
2021-07-01 18:09:04 +00:00
carrier = " land transport oil " ,
2023-10-24 12:06:17 +00:00
unit = " land transport " ,
2021-07-01 18:09:04 +00:00
)
2019-04-17 12:24:22 +00:00
2023-10-24 12:06:17 +00:00
n . madd (
" Load " ,
spatial . oil . land_transport ,
bus = spatial . oil . land_transport ,
carrier = " land transport oil " ,
p_set = ice_share / ice_efficiency * transport [ nodes ] . rename ( columns = lambda x : x + " land transport oil " ) ,
2023-03-06 08:27:45 +00:00
)
2021-03-04 17:20:23 +00:00
2023-10-24 12:06:17 +00:00
n . madd (
" Link " ,
spatial . oil . land_transport ,
bus0 = spatial . oil . nodes ,
bus1 = spatial . oil . land_transport ,
bus2 = " co2 atmosphere " ,
carrier = " land transport oil " ,
efficiency = ice_efficiency ,
efficiency2 = costs . at [ " oil " , " CO2 intensity " ] ,
p_nom_extendable = True ,
2021-07-01 18:09:04 +00:00
)
2019-04-17 12:24:22 +00:00
2022-04-03 16:55:53 +00:00
def build_heat_demand ( n ) :
2022-11-18 08:08:07 +00:00
# copy forward the daily average heat demand into each hour, so it can be multiplied by the intraday profile
2022-04-03 16:55:53 +00:00
daily_space_heat_demand = (
xr . open_dataarray ( snakemake . input . heat_demand_total )
. to_pandas ( )
. reindex ( index = n . snapshots , method = " ffill " )
2023-03-06 08:27:45 +00:00
)
2022-04-03 16:55:53 +00:00
intraday_profiles = pd . read_csv ( snakemake . input . heat_profile , index_col = 0 )
sectors = [ " residential " , " services " ]
uses = [ " water " , " space " ]
heat_demand = { }
electric_heat_supply = { }
for sector , use in product ( sectors , uses ) :
weekday = list ( intraday_profiles [ f " { sector } { use } weekday " ] )
weekend = list ( intraday_profiles [ f " { sector } { use } weekend " ] )
weekly_profile = weekday * 5 + weekend * 2
intraday_year_profile = generate_periodic_profiles (
daily_space_heat_demand . index . tz_localize ( " UTC " ) ,
nodes = daily_space_heat_demand . columns ,
weekly_profile = weekly_profile ,
)
if use == " space " :
heat_demand_shape = daily_space_heat_demand * intraday_year_profile
else :
heat_demand_shape = intraday_year_profile
heat_demand [ f " { sector } { use } " ] = (
heat_demand_shape / heat_demand_shape . sum ( )
) . multiply ( pop_weighted_energy_totals [ f " total { sector } { use } " ] ) * 1e6
electric_heat_supply [ f " { sector } { use } " ] = (
heat_demand_shape / heat_demand_shape . sum ( )
) . multiply ( pop_weighted_energy_totals [ f " electricity { sector } { use } " ] ) * 1e6
heat_demand = pd . concat ( heat_demand , axis = 1 )
electric_heat_supply = pd . concat ( electric_heat_supply , axis = 1 )
# subtract from electricity load since heat demand already in heat_demand
electric_nodes = n . loads . index [ n . loads . carrier == " electricity " ]
n . loads_t . p_set [ electric_nodes ] = (
n . loads_t . p_set [ electric_nodes ]
- electric_heat_supply . groupby ( level = 1 , axis = 1 ) . sum ( ) [ electric_nodes ]
2023-03-06 08:27:45 +00:00
)
2022-04-03 16:55:53 +00:00
return heat_demand
2021-07-01 18:09:04 +00:00
def add_heat ( n , costs ) :
2021-12-03 16:22:06 +00:00
logger . info ( " Add heat sector " )
2019-04-17 12:24:22 +00:00
2020-04-09 12:58:03 +00:00
sectors = [ " residential " , " services " ]
2019-04-17 12:24:22 +00:00
2022-04-03 16:55:53 +00:00
heat_demand = build_heat_demand ( n )
2019-04-17 12:24:22 +00:00
2021-07-08 12:41:34 +00:00
nodes , dist_fraction , urban_fraction = create_nodes_for_heat_sector ( )
2019-04-17 12:24:22 +00:00
2021-07-08 12:41:34 +00:00
# NB: must add costs of central heating afterwards (EUR 400 / kWpeak, 50a, 1% FOM from Fraunhofer ISE)
2019-04-17 12:24:22 +00:00
2021-04-19 15:20:52 +00:00
# exogenously reduce space heat demand
2021-04-30 15:57:16 +00:00
if options [ " reduce_space_heat_exogenously " ] :
2021-07-01 18:09:04 +00:00
dE = get ( options [ " reduce_space_heat_exogenously_factor " ] , investment_year )
2023-02-24 13:42:51 +00:00
logger . info ( f " Assumed space heat reduction of { dE : .2% } " )
2020-10-21 12:30:26 +00:00
for sector in sectors :
2021-07-01 18:09:04 +00:00
heat_demand [ sector + " space " ] = ( 1 - dE ) * heat_demand [ sector + " space " ]
heat_systems = [
" residential rural " ,
" services rural " ,
" residential urban decentral " ,
" services urban decentral " ,
" urban central " ,
]
2021-07-08 12:41:34 +00:00
2022-04-03 16:49:35 +00:00
cop = {
" air " : xr . open_dataarray ( snakemake . input . cop_air_total )
. to_pandas ( )
. reindex ( index = n . snapshots ) ,
" ground " : xr . open_dataarray ( snakemake . input . cop_soil_total )
. to_pandas ( )
. reindex ( index = n . snapshots ) ,
}
2022-12-28 11:48:51 +00:00
if options [ " solar_thermal " ] :
solar_thermal = (
xr . open_dataarray ( snakemake . input . solar_thermal_total )
. to_pandas ( )
. reindex ( index = n . snapshots )
2023-03-06 08:27:45 +00:00
)
2022-12-28 11:48:51 +00:00
# 1e3 converts from W/m^2 to MW/(1000m^2) = kW/m^2
solar_thermal = options [ " solar_cf_correction " ] * solar_thermal / 1e3
2022-04-03 16:49:35 +00:00
2020-10-21 12:30:26 +00:00
for name in heat_systems :
2019-08-07 09:33:29 +00:00
name_type = " central " if name == " urban central " else " decentral "
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
n . add ( " Carrier " , name + " heat " )
2019-07-16 14:00:21 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Bus " ,
nodes [ name ] + f " { name } heat " ,
location = nodes [ name ] ,
2022-06-28 16:31:45 +00:00
carrier = name + " heat " ,
unit = " MWh_th " ,
2021-07-01 18:09:04 +00:00
)
2019-07-16 14:00:21 +00:00
2019-08-07 09:33:29 +00:00
## Add heat load
2019-04-17 12:24:22 +00:00
2019-08-07 09:33:29 +00:00
for sector in sectors :
2021-07-08 12:41:34 +00:00
# heat demand weighting
2019-08-07 09:33:29 +00:00
if " rural " in name :
2021-07-01 18:09:04 +00:00
factor = 1 - urban_fraction [ nodes [ name ] ]
2021-07-08 12:41:34 +00:00
elif " urban central " in name :
factor = dist_fraction [ nodes [ name ] ]
elif " urban decentral " in name :
factor = urban_fraction [ nodes [ name ] ] - dist_fraction [ nodes [ name ] ]
else :
2021-09-29 14:13:23 +00:00
raise NotImplementedError (
f " { name } not in " f " heat systems: { heat_systems } "
)
2021-07-08 12:41:34 +00:00
2019-08-07 09:33:29 +00:00
if sector in name :
heat_load = (
heat_demand [ [ sector + " water " , sector + " space " ] ]
. groupby ( level = 1 , axis = 1 )
. sum ( ) [ nodes [ name ] ]
. multiply ( factor )
2023-03-06 08:27:45 +00:00
)
2019-04-17 12:24:22 +00:00
2019-08-07 09:33:29 +00:00
if name == " urban central " :
2021-09-29 12:37:36 +00:00
heat_load = (
heat_demand . groupby ( level = 1 , axis = 1 )
. sum ( ) [ nodes [ name ] ]
. multiply (
factor * ( 1 + options [ " district_heating " ] [ " district_heating_loss " ] )
2023-03-06 08:27:45 +00:00
)
)
2021-07-01 18:09:04 +00:00
n . madd (
" Load " ,
nodes [ name ] ,
suffix = f " { name } heat " ,
bus = nodes [ name ] + f " { name } heat " ,
carrier = name + " heat " ,
p_set = heat_load ,
)
2019-04-17 12:24:22 +00:00
2019-08-07 09:33:29 +00:00
## Add heat pumps
2019-04-17 12:24:22 +00:00
2019-08-07 09:33:29 +00:00
heat_pump_type = " air " if " urban " in name else " ground "
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
costs_name = f " { name_type } { heat_pump_type } -sourced heat pump "
efficiency = (
cop [ heat_pump_type ] [ nodes [ name ] ]
if options [ " time_dep_hp_cop " ]
else costs . at [ costs_name , " efficiency " ]
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes [ name ] ,
suffix = f " { name } { heat_pump_type } heat pump " ,
bus0 = nodes [ name ] ,
bus1 = nodes [ name ] + f " { name } heat " ,
carrier = f " { name } { heat_pump_type } heat pump " ,
efficiency = efficiency ,
capital_cost = costs . at [ costs_name , " efficiency " ]
* costs . at [ costs_name , " fixed " ] ,
p_nom_extendable = True ,
lifetime = costs . at [ costs_name , " lifetime " ] ,
)
2019-08-07 09:33:29 +00:00
if options [ " tes " ] :
2021-07-01 18:09:04 +00:00
n . add ( " Carrier " , name + " water tanks " )
n . madd (
" Bus " ,
nodes [ name ] + f " { name } water tanks " ,
location = nodes [ name ] ,
2022-06-28 16:31:45 +00:00
carrier = name + " water tanks " ,
unit = " MWh_th " ,
2021-07-01 18:09:04 +00:00
)
n . madd (
" Link " ,
nodes [ name ] + f " { name } water tanks charger " ,
bus0 = nodes [ name ] + f " { name } heat " ,
bus1 = nodes [ name ] + f " { name } water tanks " ,
efficiency = costs . at [ " water tank charger " , " efficiency " ] ,
carrier = name + " water tanks charger " ,
p_nom_extendable = True ,
)
n . madd (
" Link " ,
nodes [ name ] + f " { name } water tanks discharger " ,
bus0 = nodes [ name ] + f " { name } water tanks " ,
bus1 = nodes [ name ] + f " { name } heat " ,
carrier = name + " water tanks discharger " ,
efficiency = costs . at [ " water tank discharger " , " efficiency " ] ,
p_nom_extendable = True ,
)
if isinstance ( options [ " tes_tau " ] , dict ) :
tes_time_constant_days = options [ " tes_tau " ] [ name_type ]
else :
logger . warning (
" Deprecated: a future version will require you to specify ' tes_tau ' " ,
" for ' decentral ' and ' central ' separately. " ,
)
tes_time_constant_days = (
options [ " tes_tau " ] if name_type == " decentral " else 180.0
2023-03-06 08:27:45 +00:00
)
2021-07-08 12:41:34 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Store " ,
nodes [ name ] + f " { name } water tanks " ,
bus = nodes [ name ] + f " { name } water tanks " ,
e_cyclic = True ,
e_nom_extendable = True ,
carrier = name + " water tanks " ,
standing_loss = 1 - np . exp ( - 1 / 24 / tes_time_constant_days ) ,
2022-07-11 13:46:21 +00:00
capital_cost = costs . at [ name_type + " water tank storage " , " fixed " ] ,
2021-07-01 18:09:04 +00:00
lifetime = costs . at [ name_type + " water tank storage " , " lifetime " ] ,
)
2019-08-07 09:33:29 +00:00
if options [ " boilers " ] :
2021-07-01 18:09:04 +00:00
key = f " { name_type } resistive heater "
2021-06-18 07:45:29 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes [ name ] + f " { name } resistive heater " ,
bus0 = nodes [ name ] ,
bus1 = nodes [ name ] + f " { name } heat " ,
carrier = name + " resistive heater " ,
efficiency = costs . at [ key , " efficiency " ] ,
capital_cost = costs . at [ key , " efficiency " ] * costs . at [ key , " fixed " ] ,
p_nom_extendable = True ,
lifetime = costs . at [ key , " lifetime " ] ,
)
2019-08-07 09:33:29 +00:00
2021-07-01 18:09:04 +00:00
key = f " { name_type } gas boiler "
2019-08-07 09:33:29 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes [ name ] + f " { name } gas boiler " ,
p_nom_extendable = True ,
2021-08-04 12:47:13 +00:00
bus0 = spatial . gas . df . loc [ nodes [ name ] , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
bus1 = nodes [ name ] + f " { name } heat " ,
bus2 = " co2 atmosphere " ,
carrier = name + " gas boiler " ,
efficiency = costs . at [ key , " efficiency " ] ,
efficiency2 = costs . at [ " gas " , " CO2 intensity " ] ,
capital_cost = costs . at [ key , " efficiency " ] * costs . at [ key , " fixed " ] ,
lifetime = costs . at [ key , " lifetime " ] ,
)
2019-08-07 09:33:29 +00:00
if options [ " solar_thermal " ] :
2021-07-01 18:09:04 +00:00
n . add ( " Carrier " , name + " solar thermal " )
2019-08-07 09:33:29 +00:00
2021-07-01 18:09:04 +00:00
n . madd (
" Generator " ,
nodes [ name ] ,
suffix = f " { name } solar thermal collector " ,
bus = nodes [ name ] + f " { name } heat " ,
carrier = name + " solar thermal " ,
p_nom_extendable = True ,
capital_cost = costs . at [ name_type + " solar thermal " , " fixed " ] ,
p_max_pu = solar_thermal [ nodes [ name ] ] ,
lifetime = costs . at [ name_type + " solar thermal " , " lifetime " ] ,
)
if options [ " chp " ] and name == " urban central " :
# add gas CHP; biomass CHP is added in biomass section
n . madd (
" Link " ,
nodes [ name ] + " urban central gas CHP " ,
2021-08-04 12:47:13 +00:00
bus0 = spatial . gas . df . loc [ nodes [ name ] , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
bus1 = nodes [ name ] ,
bus2 = nodes [ name ] + " urban central heat " ,
bus3 = " co2 atmosphere " ,
carrier = " urban central gas CHP " ,
p_nom_extendable = True ,
capital_cost = costs . at [ " central gas CHP " , " fixed " ]
* costs . at [ " central gas CHP " , " efficiency " ] ,
marginal_cost = costs . at [ " central gas CHP " , " VOM " ] ,
efficiency = costs . at [ " central gas CHP " , " efficiency " ] ,
efficiency2 = costs . at [ " central gas CHP " , " efficiency " ]
/ costs . at [ " central gas CHP " , " c_b " ] ,
efficiency3 = costs . at [ " gas " , " CO2 intensity " ] ,
lifetime = costs . at [ " central gas CHP " , " lifetime " ] ,
)
n . madd (
" Link " ,
nodes [ name ] + " urban central gas CHP CC " ,
2021-08-04 12:47:13 +00:00
bus0 = spatial . gas . df . loc [ nodes [ name ] , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
bus1 = nodes [ name ] ,
bus2 = nodes [ name ] + " urban central heat " ,
bus3 = " co2 atmosphere " ,
2021-07-09 10:50:40 +00:00
bus4 = spatial . co2 . df . loc [ nodes [ name ] , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
carrier = " urban central gas CHP CC " ,
p_nom_extendable = True ,
capital_cost = costs . at [ " central gas CHP " , " fixed " ]
* costs . at [ " central gas CHP " , " efficiency " ]
+ costs . at [ " biomass CHP capture " , " fixed " ]
* costs . at [ " gas " , " CO2 intensity " ] ,
marginal_cost = costs . at [ " central gas CHP " , " VOM " ] ,
efficiency = costs . at [ " central gas CHP " , " efficiency " ]
- costs . at [ " gas " , " CO2 intensity " ]
* (
costs . at [ " biomass CHP capture " , " electricity-input " ]
+ costs . at [ " biomass CHP capture " , " compression-electricity-input " ]
) ,
efficiency2 = costs . at [ " central gas CHP " , " efficiency " ]
/ costs . at [ " central gas CHP " , " c_b " ]
+ costs . at [ " gas " , " CO2 intensity " ]
* (
costs . at [ " biomass CHP capture " , " heat-output " ]
+ costs . at [ " biomass CHP capture " , " compression-heat-output " ]
- costs . at [ " biomass CHP capture " , " heat-input " ]
2023-03-06 08:27:45 +00:00
) ,
2021-07-01 18:09:04 +00:00
efficiency3 = costs . at [ " gas " , " CO2 intensity " ]
* ( 1 - costs . at [ " biomass CHP capture " , " capture_rate " ] ) ,
efficiency4 = costs . at [ " gas " , " CO2 intensity " ]
* costs . at [ " biomass CHP capture " , " capture_rate " ] ,
lifetime = costs . at [ " central gas CHP " , " lifetime " ] ,
)
if options [ " chp " ] and options [ " micro_chp " ] and name != " urban central " :
n . madd (
" Link " ,
nodes [ name ] + f " { name } micro gas CHP " ,
p_nom_extendable = True ,
2021-08-04 12:47:13 +00:00
bus0 = spatial . gas . df . loc [ nodes [ name ] , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
bus1 = nodes [ name ] ,
bus2 = nodes [ name ] + f " { name } heat " ,
bus3 = " co2 atmosphere " ,
carrier = name + " micro gas CHP " ,
efficiency = costs . at [ " micro CHP " , " efficiency " ] ,
efficiency2 = costs . at [ " micro CHP " , " efficiency-heat " ] ,
efficiency3 = costs . at [ " gas " , " CO2 intensity " ] ,
capital_cost = costs . at [ " micro CHP " , " fixed " ] ,
lifetime = costs . at [ " micro CHP " , " lifetime " ] ,
)
2019-04-17 12:24:22 +00:00
2020-10-21 13:21:26 +00:00
if options [ " retrofitting " ] [ " retro_endogen " ] :
2021-12-03 16:22:06 +00:00
logger . info ( " Add retrofitting endogenously " )
2020-10-21 17:19:38 +00:00
2020-11-11 17:18:56 +00:00
# resample heat demand temporal 'heat_demand_r' depending on in config
# specified temporal resolution, to not overestimate retrofitting
hours = list ( filter ( re . compile ( r " ^ \ d+h$ " , re . IGNORECASE ) . search , opts ) )
if len ( hours ) == 0 :
hours = [ n . snapshots [ 1 ] - n . snapshots [ 0 ] ]
heat_demand_r = heat_demand . resample ( hours [ 0 ] ) . mean ( )
# retrofitting data 'retro_data' with 'costs' [EUR/m^2] and heat
# demand 'dE' [per unit of original heat demand] for each country and
# different retrofitting strengths [additional insulation thickness in m]
retro_data = pd . read_csv (
2023-10-11 18:43:51 +00:00
snakemake . input . retro_cost ,
2020-10-21 13:21:26 +00:00
index_col = [ 0 , 1 ] ,
skipinitialspace = True ,
header = [ 0 , 1 ] ,
)
2020-11-11 17:18:56 +00:00
# heated floor area [10^6 * m^2] per country
2020-10-21 13:21:26 +00:00
floor_area = pd . read_csv ( snakemake . input . floor_area , index_col = [ 0 , 1 ] )
2021-07-01 18:09:04 +00:00
n . add ( " Carrier " , " retrofitting " )
2020-10-21 13:21:26 +00:00
2020-11-11 17:18:56 +00:00
# share of space heat demand 'w_space' of total heat demand
2020-10-21 13:21:26 +00:00
w_space = { }
for sector in sectors :
w_space [ sector ] = heat_demand_r [ sector + " space " ] / (
heat_demand_r [ sector + " space " ] + heat_demand_r [ sector + " water " ]
)
w_space [ " tot " ] = (
heat_demand_r [ " services space " ] + heat_demand_r [ " residential space " ]
) / heat_demand_r . groupby ( level = [ 1 ] , axis = 1 ) . sum ( )
2020-10-21 17:19:38 +00:00
2021-07-01 18:09:04 +00:00
for name in n . loads [
n . loads . carrier . isin ( [ x + " heat " for x in heat_systems ] )
2023-03-06 08:27:45 +00:00
] . index :
2021-07-01 18:09:04 +00:00
node = n . buses . loc [ name , " location " ]
2020-11-11 17:18:56 +00:00
ct = pop_layout . loc [ node , " ct " ]
2020-10-21 17:19:38 +00:00
2020-11-11 17:18:56 +00:00
# weighting 'f' depending on the size of the population at the node
f = urban_fraction [ node ] if " urban " in name else ( 1 - urban_fraction [ node ] )
2020-10-21 17:19:38 +00:00
if f == 0 :
continue
2020-11-11 17:18:56 +00:00
# get sector name ("residential"/"services"/or both "tot" for urban central)
2020-10-21 17:19:38 +00:00
sec = [ x if x in name else " tot " for x in sectors ] [ 0 ]
2020-11-11 17:18:56 +00:00
# get floor aread at node and region (urban/rural) in m^2
floor_area_node = (
2020-10-21 17:19:38 +00:00
pop_layout . loc [ node ] . fraction * floor_area . loc [ ct , " value " ] * 10 * * 6
) . loc [ sec ] * f
2020-11-11 17:18:56 +00:00
# total heat demand at node [MWh]
2021-07-01 18:09:04 +00:00
demand = n . loads_t . p_set [ name ] . resample ( hours [ 0 ] ) . mean ( )
2020-11-11 17:18:56 +00:00
# space heat demand at node [MWh]
2020-10-21 17:19:38 +00:00
space_heat_demand = demand * w_space [ sec ] [ node ]
2020-11-11 17:18:56 +00:00
# normed time profile of space heat demand 'space_pu' (values between 0-1),
2020-10-21 17:19:38 +00:00
# p_max_pu/p_min_pu of retrofitting generators
space_pu = ( space_heat_demand / space_heat_demand . max ( ) ) . to_frame ( name = node )
2020-11-11 17:18:56 +00:00
# minimum heat demand 'dE' after retrofitting in units of original heat demand (values between 0-1)
dE = retro_data . loc [ ( ct , sec ) , ( " dE " ) ]
2022-11-18 08:08:07 +00:00
# get additional energy savings 'dE_diff' between the different retrofitting strengths/generators at one node
2020-10-21 17:19:38 +00:00
dE_diff = abs ( dE . diff ( ) ) . fillna ( 1 - dE . iloc [ 0 ] )
2020-11-11 17:18:56 +00:00
# convert costs Euro/m^2 -> Euro/MWh
capital_cost = (
retro_data . loc [ ( ct , sec ) , ( " cost " ) ]
* floor_area_node
2020-10-21 17:19:38 +00:00
/ ( ( 1 - dE ) * space_heat_demand . max ( ) )
2023-03-06 08:27:45 +00:00
)
2020-11-11 17:18:56 +00:00
# number of possible retrofitting measures 'strengths' (set in list at config.yaml 'l_strength')
# given in additional insulation thickness [m]
# for each measure, a retrofitting generator is added at the node
strengths = retro_data . columns . levels [ 1 ]
2020-10-21 17:19:38 +00:00
2020-11-11 17:18:56 +00:00
# check that ambitious retrofitting has higher costs per MWh than moderate retrofitting
2020-10-21 17:19:38 +00:00
if ( capital_cost . diff ( ) < 0 ) . sum ( ) :
2021-12-03 16:22:06 +00:00
logger . warning ( f " Costs are not linear for { ct } { sec } " )
2020-10-21 17:19:38 +00:00
s = capital_cost [ ( capital_cost . diff ( ) < 0 ) ] . index
strengths = strengths . drop ( s )
2020-11-11 17:18:56 +00:00
# reindex normed time profile of space heat demand back to hourly resolution
2021-07-01 18:09:04 +00:00
space_pu = space_pu . reindex ( index = heat_demand . index ) . fillna ( method = " ffill " )
2020-10-21 17:19:38 +00:00
2020-11-11 17:18:56 +00:00
# add for each retrofitting strength a generator with heat generation profile following the profile of the heat demand
2020-10-21 17:19:38 +00:00
for strength in strengths :
2021-07-01 18:09:04 +00:00
n . madd (
" Generator " ,
[ node ] ,
suffix = " retrofitting " + strength + " " + name [ 6 : : ] ,
bus = name ,
carrier = " retrofitting " ,
p_nom_extendable = True ,
p_nom_max = dE_diff [ strength ]
* space_heat_demand . max ( ) , # maximum energy savings for this renovation strength
p_max_pu = space_pu ,
p_min_pu = space_pu ,
country = ct ,
capital_cost = capital_cost [ strength ]
* options [ " retrofitting " ] [ " cost_factor " ] ,
)
2020-10-21 13:21:26 +00:00
2020-04-09 12:58:03 +00:00
def create_nodes_for_heat_sector ( ) :
2021-07-01 18:09:04 +00:00
# TODO pop_layout
2020-04-09 12:58:03 +00:00
# rural are areas with low heating density and individual heating
# urban are areas with high heating density
# urban can be split into district heating (central) and individual heating (decentral)
2021-07-08 12:41:34 +00:00
2021-09-29 14:13:23 +00:00
ct_urban = pop_layout . urban . groupby ( pop_layout . ct ) . sum ( )
2021-09-23 12:10:56 +00:00
# distribution of urban population within a country
2021-09-29 14:13:23 +00:00
pop_layout [ " urban_ct_fraction " ] = pop_layout . urban / pop_layout . ct . map ( ct_urban . get )
2021-07-08 12:41:34 +00:00
2021-07-01 18:09:04 +00:00
sectors = [ " residential " , " services " ]
2021-07-08 12:41:34 +00:00
2021-07-01 18:09:04 +00:00
nodes = { }
2021-09-29 14:13:23 +00:00
urban_fraction = pop_layout . urban / pop_layout [ [ " rural " , " urban " ] ] . sum ( axis = 1 )
2021-07-08 12:41:34 +00:00
2020-04-09 12:58:03 +00:00
for sector in sectors :
nodes [ sector + " rural " ] = pop_layout . index
2021-07-08 12:41:34 +00:00
nodes [ sector + " urban decentral " ] = pop_layout . index
2022-04-03 16:49:35 +00:00
district_heat_share = pop_weighted_energy_totals [ " district heat share " ]
2021-09-29 12:37:36 +00:00
# maximum potential of urban demand covered by district heating
central_fraction = options [ " district_heating " ] [ " potential " ]
# district heating share at each node
dist_fraction_node = (
district_heat_share * pop_layout [ " urban_ct_fraction " ] / pop_layout [ " fraction " ]
2023-03-06 08:27:45 +00:00
)
2021-09-29 12:37:36 +00:00
nodes [ " urban central " ] = dist_fraction_node . index
# if district heating share larger than urban fraction -> set urban
# fraction to district heating share
urban_fraction = pd . concat ( [ urban_fraction , dist_fraction_node ] , axis = 1 ) . max ( axis = 1 )
# difference of max potential and today's share of district heating
diff = ( urban_fraction * central_fraction ) - dist_fraction_node
2021-10-04 12:28:59 +00:00
progress = get ( options [ " district_heating " ] [ " progress " ] , investment_year )
2021-09-29 12:37:36 +00:00
dist_fraction_node + = diff * progress
2023-02-16 17:42:19 +00:00
logger . info (
f " Increase district heating share by a progress factor of { progress : .2% } "
f " resulting in new average share of { dist_fraction_node . mean ( ) : .2% } "
2021-09-30 15:46:26 +00:00
)
2020-04-09 12:58:03 +00:00
2021-09-29 12:37:36 +00:00
return nodes , dist_fraction_node , urban_fraction
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
def add_biomass ( n , costs ) :
2021-12-03 16:22:06 +00:00
logger . info ( " Add biomass " )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
biomass_potentials = pd . read_csv ( snakemake . input . biomass_potentials , index_col = 0 )
2019-04-17 12:24:22 +00:00
2021-08-04 12:47:13 +00:00
# need to aggregate potentials if gas not nodally resolved
2021-11-02 18:03:26 +00:00
if options [ " gas_network " ] :
biogas_potentials_spatial = biomass_potentials [ " biogas " ] . rename (
index = lambda x : x + " biogas "
)
else :
biogas_potentials_spatial = biomass_potentials [ " biogas " ] . sum ( )
2023-01-24 17:44:39 +00:00
if options . get ( " biomass_spatial " , options [ " biomass_transport " ] ) :
2021-11-02 18:03:26 +00:00
solid_biomass_potentials_spatial = biomass_potentials [ " solid biomass " ] . rename (
index = lambda x : x + " solid biomass "
)
2021-08-09 15:51:37 +00:00
else :
2021-11-02 18:03:26 +00:00
solid_biomass_potentials_spatial = biomass_potentials [ " solid biomass " ] . sum ( )
2021-06-18 07:45:29 +00:00
2021-07-02 08:12:43 +00:00
n . add ( " Carrier " , " biogas " )
2021-07-01 18:09:04 +00:00
n . add ( " Carrier " , " solid biomass " )
2021-07-02 13:00:19 +00:00
n . madd (
" Bus " ,
2021-08-04 12:47:13 +00:00
spatial . gas . biogas ,
location = spatial . gas . locations ,
2022-06-28 16:31:45 +00:00
carrier = " biogas " ,
2022-06-30 15:37:40 +00:00
unit = " MWh_LHV " ,
2021-07-01 18:09:04 +00:00
)
2021-07-02 08:12:43 +00:00
n . madd (
" Bus " ,
2021-07-12 10:31:18 +00:00
spatial . biomass . nodes ,
location = spatial . biomass . locations ,
2022-06-28 16:31:45 +00:00
carrier = " solid biomass " ,
2022-06-30 15:37:40 +00:00
unit = " MWh_LHV " ,
2021-07-01 18:09:04 +00:00
)
2021-07-02 13:00:19 +00:00
n . madd (
" Store " ,
2021-08-04 12:47:13 +00:00
spatial . gas . biogas ,
bus = spatial . gas . biogas ,
2021-07-01 18:09:04 +00:00
carrier = " biogas " ,
2021-11-02 18:03:26 +00:00
e_nom = biogas_potentials_spatial ,
2021-07-01 18:09:04 +00:00
marginal_cost = costs . at [ " biogas " , " fuel " ] ,
2021-11-02 18:03:26 +00:00
e_initial = biogas_potentials_spatial ,
2021-07-01 18:09:04 +00:00
)
2021-07-02 08:12:43 +00:00
n . madd (
" Store " ,
2021-07-12 10:31:18 +00:00
spatial . biomass . nodes ,
bus = spatial . biomass . nodes ,
2021-07-01 18:09:04 +00:00
carrier = " solid biomass " ,
2021-11-02 18:03:26 +00:00
e_nom = solid_biomass_potentials_spatial ,
2021-07-01 18:09:04 +00:00
marginal_cost = costs . at [ " solid biomass " , " fuel " ] ,
2021-11-02 18:03:26 +00:00
e_initial = solid_biomass_potentials_spatial ,
2021-07-01 18:09:04 +00:00
)
2021-07-02 13:00:19 +00:00
n . madd (
" Link " ,
2021-12-03 16:22:06 +00:00
spatial . gas . biogas_to_gas ,
2021-08-04 12:47:13 +00:00
bus0 = spatial . gas . biogas ,
bus1 = spatial . gas . nodes ,
2021-07-01 18:09:04 +00:00
bus2 = " co2 atmosphere " ,
carrier = " biogas to gas " ,
capital_cost = costs . loc [ " biogas upgrading " , " fixed " ] ,
marginal_cost = costs . loc [ " biogas upgrading " , " VOM " ] ,
efficiency2 = - costs . at [ " gas " , " CO2 intensity " ] ,
p_nom_extendable = True ,
)
2019-04-17 12:24:22 +00:00
2023-07-31 10:23:03 +00:00
if options [ " biomass_transport " ] :
2021-07-12 10:31:18 +00:00
# add biomass transport
2023-08-02 12:39:20 +00:00
transport_costs = pd . read_csv (
snakemake . input . biomass_transport_costs , index_col = 0
)
transport_costs = transport_costs . squeeze ( )
2021-07-12 10:31:18 +00:00
biomass_transport = create_network_topology (
n , " biomass transport " , bidirectional = False
)
2020-10-20 11:46:39 +00:00
2021-07-12 10:31:18 +00:00
# costs
bus0_costs = biomass_transport . bus0 . apply ( lambda x : transport_costs [ x [ : 2 ] ] )
bus1_costs = biomass_transport . bus1 . apply ( lambda x : transport_costs [ x [ : 2 ] ] )
biomass_transport [ " costs " ] = pd . concat ( [ bus0_costs , bus1_costs ] , axis = 1 ) . mean (
axis = 1
)
2020-10-20 11:46:39 +00:00
2021-07-12 10:31:18 +00:00
n . madd (
" Link " ,
biomass_transport . index ,
bus0 = biomass_transport . bus0 + " solid biomass " ,
bus1 = biomass_transport . bus1 + " solid biomass " ,
2023-01-25 07:41:08 +00:00
p_nom_extendable = False ,
p_nom = 5e4 ,
2021-07-12 10:31:18 +00:00
length = biomass_transport . length . values ,
marginal_cost = biomass_transport . costs * biomass_transport . length . values ,
carrier = " solid biomass transport " ,
)
2019-07-16 14:00:21 +00:00
2023-07-31 10:23:03 +00:00
elif options [ " biomass_spatial " ] :
# add artificial biomass generators at nodes which include transport costs
2023-08-02 12:39:20 +00:00
transport_costs = pd . read_csv (
snakemake . input . biomass_transport_costs , index_col = 0
)
transport_costs = transport_costs . squeeze ( )
2023-07-31 10:23:03 +00:00
bus_transport_costs = spatial . biomass . nodes . to_series ( ) . apply (
lambda x : transport_costs [ x [ : 2 ] ]
)
average_distance = 200 # km #TODO: validate this assumption
n . madd (
" Generator " ,
spatial . biomass . nodes ,
bus = spatial . biomass . nodes ,
carrier = " solid biomass " ,
p_nom = 10000 ,
marginal_cost = costs . at [ " solid biomass " , " fuel " ]
+ bus_transport_costs * average_distance ,
)
2019-07-16 14:00:21 +00:00
# AC buses with district heating
2021-07-01 18:09:04 +00:00
urban_central = n . buses . index [ n . buses . carrier == " urban central heat " ]
2019-12-12 14:03:51 +00:00
if not urban_central . empty and options [ " chp " ] :
2019-07-16 14:00:21 +00:00
urban_central = urban_central . str [ : - len ( " urban central heat " ) ]
2021-07-01 18:09:04 +00:00
key = " central solid biomass CHP "
n . madd (
" Link " ,
urban_central + " urban central solid biomass CHP " ,
2021-07-12 10:31:18 +00:00
bus0 = spatial . biomass . df . loc [ urban_central , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
bus1 = urban_central ,
bus2 = urban_central + " urban central heat " ,
carrier = " urban central solid biomass CHP " ,
p_nom_extendable = True ,
capital_cost = costs . at [ key , " fixed " ] * costs . at [ key , " efficiency " ] ,
marginal_cost = costs . at [ key , " VOM " ] ,
efficiency = costs . at [ key , " efficiency " ] ,
efficiency2 = costs . at [ key , " efficiency-heat " ] ,
lifetime = costs . at [ key , " lifetime " ] ,
)
n . madd (
" Link " ,
urban_central + " urban central solid biomass CHP CC " ,
2021-07-12 10:31:18 +00:00
bus0 = spatial . biomass . df . loc [ urban_central , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
bus1 = urban_central ,
bus2 = urban_central + " urban central heat " ,
bus3 = " co2 atmosphere " ,
2021-07-09 10:50:40 +00:00
bus4 = spatial . co2 . df . loc [ urban_central , " nodes " ] . values ,
2021-07-01 18:09:04 +00:00
carrier = " urban central solid biomass CHP CC " ,
p_nom_extendable = True ,
capital_cost = costs . at [ key , " fixed " ] * costs . at [ key , " efficiency " ]
+ costs . at [ " biomass CHP capture " , " fixed " ]
* costs . at [ " solid biomass " , " CO2 intensity " ] ,
marginal_cost = costs . at [ key , " VOM " ] ,
efficiency = costs . at [ key , " efficiency " ]
- costs . at [ " solid biomass " , " CO2 intensity " ]
* (
costs . at [ " biomass CHP capture " , " electricity-input " ]
+ costs . at [ " biomass CHP capture " , " compression-electricity-input " ]
) ,
efficiency2 = costs . at [ key , " efficiency-heat " ]
+ costs . at [ " solid biomass " , " CO2 intensity " ]
* (
costs . at [ " biomass CHP capture " , " heat-output " ]
+ costs . at [ " biomass CHP capture " , " compression-heat-output " ]
- costs . at [ " biomass CHP capture " , " heat-input " ]
) ,
efficiency3 = - costs . at [ " solid biomass " , " CO2 intensity " ]
* costs . at [ " biomass CHP capture " , " capture_rate " ] ,
efficiency4 = costs . at [ " solid biomass " , " CO2 intensity " ]
* costs . at [ " biomass CHP capture " , " capture_rate " ] ,
lifetime = costs . at [ key , " lifetime " ] ,
)
2022-08-05 13:55:41 +00:00
if options [ " biomass_boiler " ] :
2022-08-05 10:19:47 +00:00
# TODO: Add surcharge for pellets
nodes_heat = create_nodes_for_heat_sector ( ) [ 0 ]
for name in [
" residential rural " ,
" services rural " ,
" residential urban decentral " ,
" services urban decentral " ,
] :
n . madd (
" Link " ,
nodes_heat [ name ] + f " { name } biomass boiler " ,
p_nom_extendable = True ,
bus0 = spatial . biomass . df . loc [ nodes_heat [ name ] , " nodes " ] . values ,
bus1 = nodes_heat [ name ] + f " { name } heat " ,
carrier = name + " biomass boiler " ,
efficiency = costs . at [ " biomass boiler " , " efficiency " ] ,
capital_cost = costs . at [ " biomass boiler " , " efficiency " ]
* costs . at [ " biomass boiler " , " fixed " ] ,
lifetime = costs . at [ " biomass boiler " , " lifetime " ] ,
)
2021-07-01 18:09:04 +00:00
2022-08-01 11:07:59 +00:00
# Solid biomass to liquid fuel
2022-08-01 13:00:27 +00:00
if options [ " biomass_to_liquid " ] :
2022-08-01 11:07:59 +00:00
n . madd (
" Link " ,
2022-08-01 13:00:27 +00:00
spatial . biomass . nodes ,
suffix = " biomass to liquid " ,
2022-08-01 11:07:59 +00:00
bus0 = spatial . biomass . nodes ,
2022-08-01 11:26:51 +00:00
bus1 = spatial . oil . nodes ,
2022-08-04 14:03:16 +00:00
bus2 = " co2 atmosphere " ,
2022-08-01 11:07:59 +00:00
carrier = " biomass to liquid " ,
lifetime = costs . at [ " BtL " , " lifetime " ] ,
efficiency = costs . at [ " BtL " , " efficiency " ] ,
2022-08-04 14:03:16 +00:00
efficiency2 = - costs . at [ " solid biomass " , " CO2 intensity " ]
+ costs . at [ " BtL " , " CO2 stored " ] ,
2022-08-01 11:07:59 +00:00
p_nom_extendable = True ,
capital_cost = costs . at [ " BtL " , " fixed " ] ,
marginal_cost = costs . at [ " BtL " , " efficiency " ] * costs . loc [ " BtL " , " VOM " ] ,
2022-08-01 13:00:27 +00:00
)
2023-03-06 08:27:45 +00:00
2022-08-01 11:07:59 +00:00
# TODO: Update with energy penalty
n . madd (
" Link " ,
2022-08-01 13:00:27 +00:00
spatial . biomass . nodes ,
suffix = " biomass to liquid CC " ,
bus0 = spatial . biomass . nodes ,
bus1 = spatial . oil . nodes ,
2022-08-04 14:03:16 +00:00
bus2 = " co2 atmosphere " ,
bus3 = spatial . co2 . nodes ,
2022-08-01 13:00:27 +00:00
carrier = " biomass to liquid " ,
lifetime = costs . at [ " BtL " , " lifetime " ] ,
efficiency = costs . at [ " BtL " , " efficiency " ] ,
2022-08-04 14:03:16 +00:00
efficiency2 = - costs . at [ " solid biomass " , " CO2 intensity " ]
+ costs . at [ " BtL " , " CO2 stored " ] * ( 1 - costs . at [ " BtL " , " capture rate " ] ) ,
efficiency3 = costs . at [ " BtL " , " CO2 stored " ] * costs . at [ " BtL " , " capture rate " ] ,
2022-08-01 13:00:27 +00:00
p_nom_extendable = True ,
capital_cost = costs . at [ " BtL " , " fixed " ]
+ costs . at [ " biomass CHP capture " , " fixed " ] * costs . at [ " BtL " , " CO2 stored " ] ,
2022-08-05 13:55:41 +00:00
marginal_cost = costs . at [ " BtL " , " efficiency " ] * costs . loc [ " BtL " , " VOM " ] ,
)
2023-03-06 08:27:45 +00:00
2022-08-01 11:22:58 +00:00
# BioSNG from solid biomass
if options [ " biosng " ] :
n . madd (
" Link " ,
2022-08-01 13:13:59 +00:00
spatial . biomass . nodes ,
suffix = " solid biomass to gas " ,
bus0 = spatial . biomass . nodes ,
bus1 = spatial . gas . nodes ,
bus3 = " co2 atmosphere " ,
carrier = " BioSNG " ,
lifetime = costs . at [ " BioSNG " , " lifetime " ] ,
efficiency = costs . at [ " BioSNG " , " efficiency " ] ,
efficiency3 = - costs . at [ " solid biomass " , " CO2 intensity " ]
+ costs . at [ " BioSNG " , " CO2 stored " ] ,
p_nom_extendable = True ,
capital_cost = costs . at [ " BioSNG " , " fixed " ] ,
marginal_cost = costs . at [ " BioSNG " , " efficiency " ] * costs . loc [ " BioSNG " , " VOM " ] ,
)
2022-08-01 11:22:58 +00:00
# TODO: Update with energy penalty for CC
n . madd (
" Link " ,
2022-08-01 13:13:59 +00:00
spatial . biomass . nodes ,
suffix = " solid biomass to gas CC " ,
bus0 = spatial . biomass . nodes ,
bus1 = spatial . gas . nodes ,
bus2 = spatial . co2 . nodes ,
bus3 = " co2 atmosphere " ,
carrier = " BioSNG " ,
lifetime = costs . at [ " BioSNG " , " lifetime " ] ,
efficiency = costs . at [ " BioSNG " , " efficiency " ] ,
efficiency2 = costs . at [ " BioSNG " , " CO2 stored " ]
* costs . at [ " BioSNG " , " capture rate " ] ,
efficiency3 = - costs . at [ " solid biomass " , " CO2 intensity " ]
+ costs . at [ " BioSNG " , " CO2 stored " ]
* ( 1 - costs . at [ " BioSNG " , " capture rate " ] ) ,
p_nom_extendable = True ,
capital_cost = costs . at [ " BioSNG " , " fixed " ]
+ costs . at [ " biomass CHP capture " , " fixed " ]
* costs . at [ " BioSNG " , " CO2 stored " ] ,
marginal_cost = costs . at [ " BioSNG " , " efficiency " ] * costs . loc [ " BioSNG " , " VOM " ] ,
2022-08-01 13:00:27 +00:00
)
2021-07-01 18:09:04 +00:00
def add_industry ( n , costs ) :
2021-12-03 16:22:06 +00:00
logger . info ( " Add industrial demand " )
2019-04-17 12:24:22 +00:00
nodes = pop_layout . index
2023-03-10 14:58:53 +00:00
nhours = n . snapshot_weightings . generators . sum ( )
nyears = nhours / 8760
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
# 1e6 to convert TWh to MWh
industrial_demand = (
pd . read_csv ( snakemake . input . industrial_demand , index_col = 0 ) * 1e6
2023-03-10 13:12:22 +00:00
) * nyears
2019-07-19 08:21:12 +00:00
2021-07-02 08:12:43 +00:00
n . madd (
" Bus " ,
2021-08-09 15:51:37 +00:00
spatial . biomass . industry ,
location = spatial . biomass . locations ,
2022-06-28 16:31:45 +00:00
carrier = " solid biomass for industry " ,
2022-06-30 15:37:40 +00:00
unit = " MWh_LHV " ,
2021-07-01 18:09:04 +00:00
)
2023-01-24 17:44:39 +00:00
if options . get ( " biomass_spatial " , options [ " biomass_transport " ] ) :
2021-08-09 15:51:37 +00:00
p_set = (
industrial_demand . loc [ spatial . biomass . locations , " solid biomass " ] . rename (
index = lambda x : x + " solid biomass for industry "
)
2023-03-10 14:58:53 +00:00
/ nhours
2023-03-06 08:27:45 +00:00
)
2021-08-09 15:51:37 +00:00
else :
2023-03-10 14:58:53 +00:00
p_set = industrial_demand [ " solid biomass " ] . sum ( ) / nhours
2021-07-02 08:12:43 +00:00
n . madd (
" Load " ,
2021-08-09 15:51:37 +00:00
spatial . biomass . industry ,
bus = spatial . biomass . industry ,
2021-07-01 18:09:04 +00:00
carrier = " solid biomass for industry " ,
2021-07-02 08:12:43 +00:00
p_set = p_set ,
2021-07-01 18:09:04 +00:00
)
2021-07-02 08:12:43 +00:00
n . madd (
" Link " ,
2021-08-09 15:51:37 +00:00
spatial . biomass . industry ,
bus0 = spatial . biomass . nodes ,
bus1 = spatial . biomass . industry ,
2021-07-01 18:09:04 +00:00
carrier = " solid biomass for industry " ,
p_nom_extendable = True ,
efficiency = 1.0 ,
)
2021-07-02 08:12:43 +00:00
n . madd (
" Link " ,
2021-08-09 15:51:37 +00:00
spatial . biomass . industry_cc ,
bus0 = spatial . biomass . nodes ,
bus1 = spatial . biomass . industry ,
2021-07-01 18:09:04 +00:00
bus2 = " co2 atmosphere " ,
2021-07-09 11:22:00 +00:00
bus3 = spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
carrier = " solid biomass for industry CC " ,
p_nom_extendable = True ,
capital_cost = costs . at [ " cement capture " , " fixed " ]
* costs . at [ " solid biomass " , " CO2 intensity " ] ,
efficiency = 0.9 , # TODO: make config option
efficiency2 = - costs . at [ " solid biomass " , " CO2 intensity " ]
* costs . at [ " cement capture " , " capture_rate " ] ,
efficiency3 = costs . at [ " solid biomass " , " CO2 intensity " ]
* costs . at [ " cement capture " , " capture_rate " ] ,
lifetime = costs . at [ " cement capture " , " lifetime " ] ,
)
2021-07-02 13:00:19 +00:00
n . madd (
" Bus " ,
2021-08-04 12:47:13 +00:00
spatial . gas . industry ,
location = spatial . gas . locations ,
2022-06-28 16:31:45 +00:00
carrier = " gas for industry " ,
unit = " MWh_LHV " ,
)
2021-07-01 18:09:04 +00:00
2023-03-10 14:58:53 +00:00
gas_demand = industrial_demand . loc [ nodes , " methane " ] / nhours
2021-11-03 19:34:43 +00:00
if options [ " gas_network " ] :
spatial_gas_demand = gas_demand . rename ( index = lambda x : x + " gas for industry " )
else :
spatial_gas_demand = gas_demand . sum ( )
2021-07-02 13:00:19 +00:00
n . madd (
" Load " ,
2021-08-04 12:47:13 +00:00
spatial . gas . industry ,
bus = spatial . gas . industry ,
2021-07-01 18:09:04 +00:00
carrier = " gas for industry " ,
2021-11-03 19:34:43 +00:00
p_set = spatial_gas_demand ,
2021-07-01 18:09:04 +00:00
)
2021-07-02 13:00:19 +00:00
n . madd (
" Link " ,
2021-08-04 12:47:13 +00:00
spatial . gas . industry ,
bus0 = spatial . gas . nodes ,
bus1 = spatial . gas . industry ,
2021-07-01 18:09:04 +00:00
bus2 = " co2 atmosphere " ,
carrier = " gas for industry " ,
p_nom_extendable = True ,
efficiency = 1.0 ,
efficiency2 = costs . at [ " gas " , " CO2 intensity " ] ,
)
2021-07-02 13:00:19 +00:00
n . madd (
" Link " ,
2021-08-04 12:47:13 +00:00
spatial . gas . industry_cc ,
bus0 = spatial . gas . nodes ,
bus1 = spatial . gas . industry ,
2021-07-01 18:09:04 +00:00
bus2 = " co2 atmosphere " ,
2021-07-09 11:22:00 +00:00
bus3 = spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
carrier = " gas for industry CC " ,
p_nom_extendable = True ,
capital_cost = costs . at [ " cement capture " , " fixed " ]
* costs . at [ " gas " , " CO2 intensity " ] ,
efficiency = 0.9 ,
efficiency2 = costs . at [ " gas " , " CO2 intensity " ]
* ( 1 - costs . at [ " cement capture " , " capture_rate " ] ) ,
efficiency3 = costs . at [ " gas " , " CO2 intensity " ]
* costs . at [ " cement capture " , " capture_rate " ] ,
lifetime = costs . at [ " cement capture " , " lifetime " ] ,
)
n . madd (
" Load " ,
nodes ,
suffix = " H2 for industry " ,
bus = nodes + " H2 " ,
carrier = " H2 for industry " ,
2023-03-10 14:58:53 +00:00
p_set = industrial_demand . loc [ nodes , " hydrogen " ] / nhours ,
2021-07-01 18:09:04 +00:00
)
2022-12-03 11:22:05 +00:00
shipping_hydrogen_share = get ( options [ " shipping_hydrogen_share " ] , investment_year )
shipping_methanol_share = get ( options [ " shipping_methanol_share " ] , investment_year )
shipping_oil_share = get ( options [ " shipping_oil_share " ] , investment_year )
2021-08-04 16:28:18 +00:00
2022-12-03 11:22:05 +00:00
total_share = shipping_hydrogen_share + shipping_methanol_share + shipping_oil_share
if total_share != 1 :
2023-02-16 17:42:19 +00:00
logger . warning (
f " Total shipping shares sum up to { total_share : .2% } , corresponding to increased or decreased demand assumptions. "
)
2021-08-04 16:28:18 +00:00
2022-12-28 12:45:42 +00:00
domestic_navigation = pop_weighted_energy_totals . loc [
nodes , " total domestic navigation "
] . squeeze ( )
2023-03-10 13:12:22 +00:00
international_navigation = (
pd . read_csv ( snakemake . input . shipping_demand , index_col = 0 ) . squeeze ( ) * nyears
)
2022-11-13 12:43:05 +00:00
all_navigation = domestic_navigation + international_navigation
2023-03-10 14:58:53 +00:00
p_set = all_navigation * 1e6 / nhours
2021-08-04 16:28:18 +00:00
2022-12-03 11:22:05 +00:00
if shipping_hydrogen_share :
2023-01-03 07:38:10 +00:00
oil_efficiency = options . get (
" shipping_oil_efficiency " , options . get ( " shipping_average_efficiency " , 0.4 )
2023-03-06 08:27:45 +00:00
)
2023-01-03 07:38:10 +00:00
efficiency = oil_efficiency / costs . at [ " fuel cell " , " efficiency " ]
2022-11-13 12:43:05 +00:00
shipping_hydrogen_share = get (
options [ " shipping_hydrogen_share " ] , investment_year
)
2022-12-27 10:16:39 +00:00
2022-12-03 11:22:05 +00:00
if options [ " shipping_hydrogen_liquefaction " ] :
n . madd (
" Bus " ,
nodes ,
suffix = " H2 liquid " ,
carrier = " H2 liquid " ,
location = nodes ,
unit = " MWh_LHV " ,
)
n . madd (
" Link " ,
nodes + " H2 liquefaction " ,
bus0 = nodes + " H2 " ,
bus1 = nodes + " H2 liquid " ,
carrier = " H2 liquefaction " ,
efficiency = costs . at [ " H2 liquefaction " , " efficiency " ] ,
capital_cost = costs . at [ " H2 liquefaction " , " fixed " ] ,
p_nom_extendable = True ,
lifetime = costs . at [ " H2 liquefaction " , " lifetime " ] ,
)
shipping_bus = nodes + " H2 liquid "
else :
shipping_bus = nodes + " H2 "
efficiency = (
options [ " shipping_oil_efficiency " ] / costs . at [ " fuel cell " , " efficiency " ]
2023-03-06 08:27:45 +00:00
)
2022-12-03 11:22:05 +00:00
p_set_hydrogen = shipping_hydrogen_share * p_set * efficiency
n . madd (
" Load " ,
nodes ,
suffix = " H2 for shipping " ,
bus = shipping_bus ,
carrier = " H2 for shipping " ,
p_set = p_set_hydrogen ,
)
if shipping_methanol_share :
2022-11-13 17:25:32 +00:00
n . madd (
" Bus " ,
spatial . methanol . nodes ,
carrier = " methanol " ,
location = spatial . methanol . locations ,
unit = " MWh_LHV " ,
)
2023-01-05 12:42:01 +00:00
n . madd (
" Store " ,
spatial . methanol . nodes ,
suffix = " Store " ,
bus = spatial . methanol . nodes ,
e_nom_extendable = True ,
e_cyclic = True ,
carrier = " methanol " ,
)
2022-12-28 11:19:20 +00:00
n . madd (
" Link " ,
2022-12-28 12:45:42 +00:00
spatial . h2 . locations + " methanolisation " ,
2022-11-13 17:25:32 +00:00
bus0 = spatial . h2 . nodes ,
bus1 = spatial . methanol . nodes ,
bus2 = nodes ,
bus3 = spatial . co2 . nodes ,
carrier = " methanolisation " ,
p_nom_extendable = True ,
2023-02-15 12:47:57 +00:00
p_min_pu = options . get ( " min_part_load_methanolisation " , 0 ) ,
2022-11-27 17:28:57 +00:00
capital_cost = costs . at [ " methanolisation " , " fixed " ]
* options [ " MWh_MeOH_per_MWh_H2 " ] , # EUR/MW_H2/a
2022-11-13 17:25:32 +00:00
lifetime = costs . at [ " methanolisation " , " lifetime " ] ,
efficiency = options [ " MWh_MeOH_per_MWh_H2 " ] ,
2022-12-03 11:22:05 +00:00
efficiency2 = - options [ " MWh_MeOH_per_MWh_H2 " ] / options [ " MWh_MeOH_per_MWh_e " ] ,
efficiency3 = - options [ " MWh_MeOH_per_MWh_H2 " ] / options [ " MWh_MeOH_per_tCO2 " ] ,
2022-11-13 17:25:32 +00:00
)
2022-12-03 11:22:05 +00:00
efficiency = (
options [ " shipping_oil_efficiency " ] / options [ " shipping_methanol_efficiency " ]
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
2023-10-24 14:46:58 +00:00
# need to aggregate potentials if methanol not nodally resolved
if options [ " co2_budget_national " ] :
p_set_methanol = shipping_methanol_share * p_set * efficiency
else :
p_set_methanol = shipping_methanol_share * p_set . sum ( ) * efficiency
n . madd (
2023-10-24 12:06:17 +00:00
" Bus " ,
2023-10-24 14:46:58 +00:00
spatial . methanol . shipping ,
location = spatial . methanol . locations ,
2022-12-03 11:22:05 +00:00
carrier = " shipping methanol " ,
2023-10-24 12:06:17 +00:00
unit = " MWh_LHV " ,
2022-12-03 11:22:05 +00:00
)
2021-07-01 18:09:04 +00:00
2023-10-24 14:46:58 +00:00
n . madd (
2022-12-28 11:19:20 +00:00
" Load " ,
2023-10-24 14:46:58 +00:00
spatial . methanol . shipping ,
bus = spatial . methanol . shipping ,
2023-10-24 12:06:17 +00:00
carrier = " shipping methanol " ,
p_set = p_set_methanol ,
2022-12-03 11:22:05 +00:00
)
2021-08-04 16:19:02 +00:00
n . madd (
2023-10-24 12:06:17 +00:00
" Link " ,
2023-10-24 14:46:58 +00:00
spatial . methanol . shipping ,
2023-10-24 12:06:17 +00:00
bus0 = spatial . methanol . nodes ,
2023-10-24 14:46:58 +00:00
bus1 = spatial . methanol . shipping ,
2023-10-24 12:06:17 +00:00
bus2 = " co2 atmosphere " ,
carrier = " shipping methanol " ,
p_nom_extendable = True ,
efficiency2 = 1 / options [ " MWh_MeOH_per_tCO2 " ] , # CO2 intensity methanol based on stoichiometric calculation with 22.7 GJ/t methanol (32 g/mol), CO2 (44 g/mol), 277.78 MWh/TJ = 0.218 t/MWh
2021-08-04 16:19:02 +00:00
)
2022-12-28 11:19:20 +00:00
if " oil " not in n . buses . carrier . unique ( ) :
n . madd (
" Bus " ,
spatial . oil . nodes ,
location = spatial . oil . locations ,
carrier = " oil " ,
unit = " MWh_LHV " ,
)
2021-07-01 18:09:04 +00:00
2022-12-28 11:19:20 +00:00
if " oil " not in n . stores . carrier . unique ( ) :
# could correct to e.g. 0.001 EUR/kWh * annuity and O&M
n . madd (
" Store " ,
[ oil_bus + " Store " for oil_bus in spatial . oil . nodes ] ,
bus = spatial . oil . nodes ,
e_nom_extendable = True ,
e_cyclic = True ,
carrier = " oil " ,
)
2021-07-01 18:09:04 +00:00
2022-12-28 11:19:20 +00:00
if " oil " not in n . generators . carrier . unique ( ) :
n . madd (
" Generator " ,
spatial . oil . nodes ,
bus = spatial . oil . nodes ,
p_nom_extendable = True ,
carrier = " oil " ,
marginal_cost = costs . at [ " oil " , " fuel " ] ,
)
2019-04-30 10:05:36 +00:00
2023-10-24 12:06:17 +00:00
if shipping_oil_share :
2023-10-24 14:46:58 +00:00
# need to aggregate potentials if oil not nodally resolved
if options [ " co2_budget_national " ] :
p_set_oil = shipping_oil_share * p_set
else :
p_set_oil = shipping_oil_share * p_set . sum ( )
2023-10-24 12:06:17 +00:00
2023-10-24 14:46:58 +00:00
n . madd (
2023-10-24 12:06:17 +00:00
" Bus " ,
2023-10-24 14:46:58 +00:00
spatial . oil . shipping ,
location = spatial . oil . locations ,
2023-10-24 12:06:17 +00:00
carrier = " shipping oil " ,
unit = " MWh_LHV " ,
)
2023-10-24 14:46:58 +00:00
n . madd (
2023-10-24 12:06:17 +00:00
" Load " ,
2023-10-24 14:46:58 +00:00
spatial . oil . shipping ,
bus = spatial . oil . shipping ,
2023-10-24 12:06:17 +00:00
carrier = " shipping oil " ,
p_set = p_set_oil ,
)
n . madd (
" Link " ,
2023-10-24 14:46:58 +00:00
spatial . oil . shipping ,
2023-10-24 12:06:17 +00:00
bus0 = spatial . oil . nodes ,
2023-10-24 14:46:58 +00:00
bus1 = spatial . oil . shipping ,
2023-10-24 12:06:17 +00:00
bus2 = " co2 atmosphere " ,
carrier = " shipping oil " ,
p_nom_extendable = True ,
efficiency2 = costs . at [ " oil " , " CO2 intensity " ] ,
)
2020-04-09 12:58:03 +00:00
if options [ " oil_boilers " ] :
2021-07-08 12:41:34 +00:00
nodes_heat = create_nodes_for_heat_sector ( ) [ 0 ]
2020-04-09 12:58:03 +00:00
for name in [
" residential rural " ,
" services rural " ,
" residential urban decentral " ,
" services urban decentral " ,
] :
2021-07-01 18:09:04 +00:00
n . madd (
" Link " ,
nodes_heat [ name ] + f " { name } oil boiler " ,
p_nom_extendable = True ,
2022-03-21 08:14:15 +00:00
bus0 = spatial . oil . nodes ,
2021-07-01 18:09:04 +00:00
bus1 = nodes_heat [ name ] + f " { name } heat " ,
bus2 = " co2 atmosphere " ,
carrier = f " { name } oil boiler " ,
efficiency = costs . at [ " decentral oil boiler " , " efficiency " ] ,
efficiency2 = costs . at [ " oil " , " CO2 intensity " ] ,
capital_cost = costs . at [ " decentral oil boiler " , " efficiency " ]
* costs . at [ " decentral oil boiler " , " fixed " ] ,
lifetime = costs . at [ " decentral oil boiler " , " lifetime " ] ,
)
n . madd (
" Link " ,
nodes + " Fischer-Tropsch " ,
bus0 = nodes + " H2 " ,
2022-03-21 08:14:15 +00:00
bus1 = spatial . oil . nodes ,
2021-07-09 10:50:40 +00:00
bus2 = spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
carrier = " Fischer-Tropsch " ,
efficiency = costs . at [ " Fischer-Tropsch " , " efficiency " ] ,
2022-11-13 17:25:32 +00:00
capital_cost = costs . at [ " Fischer-Tropsch " , " fixed " ]
* costs . at [ " Fischer-Tropsch " , " efficiency " ] , # EUR/MW_H2/a
2021-07-01 18:09:04 +00:00
efficiency2 = - costs . at [ " oil " , " CO2 intensity " ]
* costs . at [ " Fischer-Tropsch " , " efficiency " ] ,
p_nom_extendable = True ,
2023-02-15 12:47:57 +00:00
p_min_pu = options . get ( " min_part_load_fischer_tropsch " , 0 ) ,
2021-07-01 18:09:04 +00:00
lifetime = costs . at [ " Fischer-Tropsch " , " lifetime " ] ,
)
2023-10-24 12:06:17 +00:00
# naphtha
2022-12-29 10:46:57 +00:00
demand_factor = options . get ( " HVC_demand_factor " , 1 )
if demand_factor != 1 :
logger . warning ( f " Changing HVC demand by { demand_factor * 100 - 100 : +.2f } %. " )
2023-10-24 12:06:17 +00:00
# NB: CO2 gets released again to atmosphere when plastics decay
# except for the process emissions when naphtha is used for petrochemicals, which can be captured with other industry process emissions
# convert process emissions from feedstock from MtCO2 to energy demand
2023-10-24 14:46:58 +00:00
# need to aggregate potentials if oil not nodally resolved
if options [ " co2_budget_national " ] :
2023-10-26 09:17:57 +00:00
p_set_plastics = demand_factor * ( industrial_demand . loc [ nodes , " naphtha " ] - industrial_demand . loc [ nodes , " process emission from feedstock " ] / costs . at [ " oil " , " CO2 intensity " ] ) / nhours
2023-10-24 14:46:58 +00:00
else :
2023-10-26 09:17:57 +00:00
p_set_plastics = demand_factor * ( industrial_demand . loc [ nodes , " naphtha " ] - industrial_demand . loc [ nodes , " process emission from feedstock " ] / costs . at [ " oil " , " CO2 intensity " ] ) . sum ( ) / nhours
if options [ " co2_budget_national " ] :
p_set_process_emissions = (
demand_factor
* ( industrial_demand . loc [ nodes , " process emission from feedstock " ]
/ costs . at [ " oil " , " CO2 intensity " ] )
/ nhours
)
else :
p_set_process_emissions = (
demand_factor
* ( industrial_demand . loc [ nodes , " process emission from feedstock " ]
/ costs . at [ " oil " , " CO2 intensity " ]
) . sum ( )
/ nhours
)
2023-10-24 12:06:17 +00:00
2023-10-24 14:46:58 +00:00
n . madd (
2023-10-24 12:06:17 +00:00
" Bus " ,
2023-10-24 14:46:58 +00:00
spatial . oil . naphtha ,
location = spatial . oil . locations ,
2023-10-24 12:06:17 +00:00
carrier = " naphtha for industry " ,
unit = " MWh_LHV " ,
)
2023-10-24 14:46:58 +00:00
n . madd (
2022-03-18 12:46:40 +00:00
" Load " ,
2023-10-24 14:46:58 +00:00
spatial . oil . naphtha ,
bus = spatial . oil . naphtha ,
2021-07-01 18:09:04 +00:00
carrier = " naphtha for industry " ,
2023-10-26 09:17:57 +00:00
p_set = p_set_plastics ,
)
n . madd (
" Load " ,
[ " naphtha for industry into process emissions from feedstock " ] ,
bus = spatial . oil . nodes ,
carrier = " naphtha for industry " ,
p_set = p_set_process_emissions ,
2021-07-01 18:09:04 +00:00
)
2023-10-24 12:06:17 +00:00
n . madd (
" Link " ,
2023-10-24 14:46:58 +00:00
spatial . oil . naphtha ,
2023-10-24 12:06:17 +00:00
bus0 = spatial . oil . nodes ,
2023-10-24 14:46:58 +00:00
bus1 = spatial . oil . naphtha ,
2023-10-24 12:06:17 +00:00
bus2 = " co2 atmosphere " ,
carrier = " naphtha for industry " ,
p_nom_extendable = True ,
efficiency2 = costs . at [ " oil " , " CO2 intensity " ] ,
)
# aviation
2022-12-29 10:46:57 +00:00
demand_factor = options . get ( " aviation_demand_factor " , 1 )
if demand_factor != 1 :
logger . warning ( f " Changing aviation demand by { demand_factor * 100 - 100 : +.2f } %. " )
2021-07-01 18:09:04 +00:00
2023-10-24 14:46:58 +00:00
all_aviation = [ " total international aviation " , " total domestic aviation " ]
# need to aggregate potentials if oil not nodally resolved
if options [ " co2_budget_national " ] :
p_set = (
demand_factor
* pop_weighted_energy_totals . loc [ nodes , all_aviation ] . sum ( axis = 1 )
* 1e6
/ nhours
)
else :
p_set = (
demand_factor
* pop_weighted_energy_totals . loc [ nodes , all_aviation ] . sum ( axis = 1 ) . sum ( )
* 1e6
/ nhours
)
n . madd (
2023-10-24 12:06:17 +00:00
" Bus " ,
2023-10-24 14:46:58 +00:00
spatial . oil . kerosene ,
location = spatial . oil . locations ,
2023-10-24 12:06:17 +00:00
carrier = " kerosene for aviation " ,
unit = " MWh_LHV " ,
)
2023-10-24 14:46:58 +00:00
n . madd (
2022-03-18 12:46:40 +00:00
" Load " ,
2023-10-24 14:46:58 +00:00
spatial . oil . kerosene ,
bus = spatial . oil . kerosene ,
2021-07-01 18:09:04 +00:00
carrier = " kerosene for aviation " ,
p_set = p_set ,
)
2019-07-19 08:21:12 +00:00
2023-10-24 12:06:17 +00:00
n . madd (
" Link " ,
2023-10-24 14:46:58 +00:00
spatial . oil . kerosene ,
2023-10-24 12:06:17 +00:00
bus0 = spatial . oil . nodes ,
2023-10-24 14:46:58 +00:00
bus1 = spatial . oil . kerosene ,
2023-10-24 12:06:17 +00:00
bus2 = " co2 atmosphere " ,
carrier = " kerosene for aviation " ,
p_nom_extendable = True ,
efficiency2 = costs . at [ " oil " , " CO2 intensity " ] ,
2021-07-01 18:09:04 +00:00
)
# TODO simplify bus expression
n . madd (
" Load " ,
nodes ,
suffix = " low-temperature heat for industry " ,
bus = [
node + " urban central heat "
if node + " urban central heat " in n . buses . index
else node + " services urban decentral heat "
for node in nodes
] ,
carrier = " low-temperature heat for industry " ,
2023-03-10 14:58:53 +00:00
p_set = industrial_demand . loc [ nodes , " low-temperature heat " ] / nhours ,
2021-07-01 18:09:04 +00:00
)
# remove today's industrial electricity demand by scaling down total electricity demand
for ct in n . buses . country . dropna ( ) . unique ( ) :
# TODO map onto n.bus.country
2023-03-10 13:12:22 +00:00
2021-07-01 18:09:04 +00:00
loads_i = n . loads . index [
( n . loads . index . str [ : 2 ] == ct ) & ( n . loads . carrier == " electricity " )
]
if n . loads_t . p_set [ loads_i ] . empty :
continue
factor = (
1
- industrial_demand . loc [ loads_i , " current electricity " ] . sum ( )
/ n . loads_t . p_set [ loads_i ] . sum ( ) . sum ( )
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
n . loads_t . p_set [ loads_i ] * = factor
n . madd (
" Load " ,
nodes ,
suffix = " industry electricity " ,
bus = nodes ,
carrier = " industry electricity " ,
2023-03-10 14:58:53 +00:00
p_set = industrial_demand . loc [ nodes , " electricity " ] / nhours ,
2021-07-01 18:09:04 +00:00
)
2023-02-08 21:57:01 +00:00
n . madd (
" Bus " ,
2023-02-06 08:46:24 +00:00
spatial . co2 . process_emissions ,
location = spatial . co2 . locations ,
2022-06-28 16:31:45 +00:00
carrier = " process emissions " ,
unit = " t_co2 " ,
2021-07-01 18:09:04 +00:00
)
2023-02-06 08:52:35 +00:00
sel = [ " process emission " , " process emission from feedstock " ]
2023-02-08 21:57:01 +00:00
if options [ " co2_spatial " ] or options [ " co2network " ] :
2023-02-06 08:52:35 +00:00
p_set = (
- industrial_demand . loc [ nodes , sel ]
. sum ( axis = 1 )
. rename ( index = lambda x : x + " process emissions " )
2023-03-10 14:58:53 +00:00
/ nhours
2023-03-06 08:27:45 +00:00
)
2023-02-06 08:52:35 +00:00
else :
2023-03-10 14:58:53 +00:00
p_set = - industrial_demand . loc [ nodes , sel ] . sum ( axis = 1 ) . sum ( ) / nhours
2023-02-06 08:52:35 +00:00
2021-07-01 18:09:04 +00:00
# this should be process emissions fossil+feedstock
# then need load on atmosphere for feedstock emissions that are currently going to atmosphere via Link Fischer-Tropsch demand
2023-02-08 21:57:01 +00:00
n . madd (
" Load " ,
2023-02-06 08:46:24 +00:00
spatial . co2 . process_emissions ,
bus = spatial . co2 . process_emissions ,
2021-07-01 18:09:04 +00:00
carrier = " process emissions " ,
2023-02-06 08:52:35 +00:00
p_set = p_set ,
2021-07-01 18:09:04 +00:00
)
2023-02-08 21:57:01 +00:00
n . madd (
" Link " ,
2023-02-06 08:46:24 +00:00
spatial . co2 . process_emissions ,
bus0 = spatial . co2 . process_emissions ,
2021-07-01 18:09:04 +00:00
bus1 = " co2 atmosphere " ,
carrier = " process emissions " ,
p_nom_extendable = True ,
efficiency = 1.0 ,
)
2019-12-13 17:06:38 +00:00
2020-12-09 15:38:49 +00:00
# assume enough local waste heat for CC
2021-07-09 11:22:00 +00:00
n . madd (
" Link " ,
spatial . co2 . locations ,
suffix = " process emissions CC " ,
2023-02-06 08:46:24 +00:00
bus0 = spatial . co2 . process_emissions ,
2021-07-01 18:09:04 +00:00
bus1 = " co2 atmosphere " ,
2021-07-09 11:22:00 +00:00
bus2 = spatial . co2 . nodes ,
2021-07-01 18:09:04 +00:00
carrier = " process emissions CC " ,
p_nom_extendable = True ,
capital_cost = costs . at [ " cement capture " , " fixed " ] ,
efficiency = 1 - costs . at [ " cement capture " , " capture_rate " ] ,
efficiency2 = costs . at [ " cement capture " , " capture_rate " ] ,
lifetime = costs . at [ " cement capture " , " lifetime " ] ,
)
2022-06-22 13:50:32 +00:00
if options . get ( " ammonia " ) :
2022-06-10 15:07:48 +00:00
if options [ " ammonia " ] == " regional " :
p_set = (
industrial_demand . loc [ spatial . ammonia . locations , " ammonia " ] . rename (
index = lambda x : x + " NH3 "
)
2023-03-10 14:58:53 +00:00
/ nhours
2023-03-06 08:27:45 +00:00
)
2022-06-10 15:07:48 +00:00
else :
2023-03-10 14:58:53 +00:00
p_set = industrial_demand [ " ammonia " ] . sum ( ) / nhours
2022-06-10 15:07:48 +00:00
2022-06-10 12:44:36 +00:00
n . madd (
" Load " ,
2022-06-10 14:53:37 +00:00
spatial . ammonia . nodes ,
bus = spatial . ammonia . nodes ,
2022-06-10 12:44:36 +00:00
carrier = " NH3 " ,
2022-06-10 15:07:48 +00:00
p_set = p_set ,
2022-06-10 12:44:36 +00:00
)
2023-08-09 09:02:20 +00:00
primary_steel = get (
snakemake . config [ " industry " ] [ " St_primary_fraction " ] , investment_year
)
2023-08-09 09:01:58 +00:00
dri_steel = get ( snakemake . config [ " industry " ] [ " DRI_fraction " ] , investment_year )
2023-08-09 08:11:28 +00:00
bof_steel = primary_steel - dri_steel
if bof_steel > 0 :
add_carrier_buses ( n , " coal " )
2023-08-09 08:12:49 +00:00
mwh_coal_per_mwh_coke = 1.366 # from eurostat energy balance
p_set = (
industrial_demand [ " coal " ] . sum ( )
+ mwh_coal_per_mwh_coke * industrial_demand [ " coke " ] . sum ( )
) / nhours
2023-08-09 08:11:28 +00:00
n . madd (
" Load " ,
2023-08-22 09:53:02 +00:00
spatial . coal . nodes ,
suffix = " for industry " ,
2023-08-09 08:11:28 +00:00
bus = spatial . coal . nodes ,
carrier = " coal for industry " ,
p_set = p_set ,
)
2021-07-01 18:09:04 +00:00
def add_waste_heat ( n ) :
# TODO options?
2019-05-14 09:49:32 +00:00
2021-12-03 16:22:06 +00:00
logger . info ( " Add possibility to use industrial waste heat in district heating " )
2019-05-14 09:49:32 +00:00
# AC buses with district heating
2021-07-01 18:09:04 +00:00
urban_central = n . buses . index [ n . buses . carrier == " urban central heat " ]
2019-07-16 14:00:21 +00:00
if not urban_central . empty :
urban_central = urban_central . str [ : - len ( " urban central heat " ) ]
2019-05-14 09:49:32 +00:00
2021-07-01 18:09:04 +00:00
# TODO what is the 0.95 and should it be a config option?
2019-07-16 14:00:21 +00:00
if options [ " use_fischer_tropsch_waste_heat " ] :
2021-07-01 18:09:04 +00:00
n . links . loc [ urban_central + " Fischer-Tropsch " , " bus3 " ] = (
urban_central + " urban central heat "
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
n . links . loc [ urban_central + " Fischer-Tropsch " , " efficiency3 " ] = (
0.95 - n . links . loc [ urban_central + " Fischer-Tropsch " , " efficiency " ]
2023-03-06 08:27:45 +00:00
)
2019-05-14 09:49:32 +00:00
2023-03-06 08:23:30 +00:00
# TODO integrate usable waste heat efficiency into technology-data from DEA
2023-02-15 13:01:00 +00:00
if options . get ( " use_electrolysis_waste_heat " , False ) :
n . links . loc [ urban_central + " H2 Electrolysis " , " bus2 " ] = (
urban_central + " urban central heat "
2023-03-06 08:27:45 +00:00
)
2023-02-15 13:01:00 +00:00
n . links . loc [ urban_central + " H2 Electrolysis " , " efficiency2 " ] = (
0.84 - n . links . loc [ urban_central + " H2 Electrolysis " , " efficiency " ]
2023-03-06 08:27:45 +00:00
)
2023-02-15 13:01:00 +00:00
2019-07-16 14:00:21 +00:00
if options [ " use_fuel_cell_waste_heat " ] :
2021-07-01 18:09:04 +00:00
n . links . loc [ urban_central + " H2 Fuel Cell " , " bus2 " ] = (
urban_central + " urban central heat "
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
n . links . loc [ urban_central + " H2 Fuel Cell " , " efficiency2 " ] = (
0.95 - n . links . loc [ urban_central + " H2 Fuel Cell " , " efficiency " ]
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
2019-05-14 09:49:32 +00:00
2021-07-06 16:32:35 +00:00
def add_agriculture ( n , costs ) :
2021-08-18 07:42:05 +00:00
logger . info ( " Add agriculture, forestry and fishing sector. " )
2021-07-06 16:32:35 +00:00
nodes = pop_layout . index
2023-03-10 14:58:53 +00:00
nhours = n . snapshot_weightings . generators . sum ( )
2021-07-06 16:32:35 +00:00
# electricity
n . madd (
" Load " ,
nodes ,
suffix = " agriculture electricity " ,
bus = nodes ,
carrier = " agriculture electricity " ,
2022-04-03 16:49:35 +00:00
p_set = pop_weighted_energy_totals . loc [ nodes , " total agriculture electricity " ]
* 1e6
2023-03-10 14:58:53 +00:00
/ nhours ,
2021-07-06 16:32:35 +00:00
)
# heat
n . madd (
" Load " ,
nodes ,
suffix = " agriculture heat " ,
bus = nodes + " services rural heat " ,
carrier = " agriculture heat " ,
2022-04-03 16:49:35 +00:00
p_set = pop_weighted_energy_totals . loc [ nodes , " total agriculture heat " ]
* 1e6
2023-03-10 14:58:53 +00:00
/ nhours ,
2021-07-06 16:32:35 +00:00
)
# machinery
electric_share = get (
options [ " agriculture_machinery_electric_share " ] , investment_year
)
2022-12-29 10:46:57 +00:00
oil_share = get ( options [ " agriculture_machinery_oil_share " ] , investment_year )
total_share = electric_share + oil_share
if total_share != 1 :
2023-02-16 17:42:19 +00:00
logger . warning (
f " Total agriculture machinery shares sum up to { total_share : .2% } , corresponding to increased or decreased demand assumptions. "
)
2021-07-06 16:32:35 +00:00
2022-04-03 16:49:35 +00:00
machinery_nodal_energy = pop_weighted_energy_totals . loc [
nodes , " total agriculture machinery "
2023-10-24 14:46:58 +00:00
] * 1e6
2021-07-06 16:32:35 +00:00
if electric_share > 0 :
efficiency_gain = (
options [ " agriculture_machinery_fuel_efficiency " ]
/ options [ " agriculture_machinery_electric_efficiency " ]
2023-03-06 08:27:45 +00:00
)
2021-07-06 16:32:35 +00:00
2021-08-18 14:17:59 +00:00
n . madd (
" Load " ,
2021-07-06 16:32:35 +00:00
nodes ,
suffix = " agriculture machinery electric " ,
bus = nodes ,
carrier = " agriculture machinery electric " ,
2021-08-16 14:27:25 +00:00
p_set = electric_share
/ efficiency_gain
* machinery_nodal_energy
2023-03-10 14:58:53 +00:00
/ nhours ,
2021-07-06 16:32:35 +00:00
)
2022-12-29 10:46:57 +00:00
if oil_share > 0 :
2023-10-24 14:46:58 +00:00
# need to aggregate potentials if oil not nodally resolved
if options [ " co2_budget_national " ] :
p_set = oil_share * machinery_nodal_energy / nhours
else :
p_set = oil_share * machinery_nodal_energy . sum ( ) / nhours
n . madd (
2023-10-24 12:06:17 +00:00
" Bus " ,
2023-10-24 14:46:58 +00:00
spatial . oil . agriculture_machinery ,
location = spatial . oil . locations ,
2023-10-24 12:06:17 +00:00
carrier = " agriculture machinery oil " ,
unit = " MWh_LHV " ,
)
2023-10-24 14:46:58 +00:00
n . madd (
2022-06-03 12:29:23 +00:00
" Load " ,
2023-10-24 14:46:58 +00:00
spatial . oil . agriculture_machinery ,
bus = spatial . oil . agriculture_machinery ,
2021-07-06 16:32:35 +00:00
carrier = " agriculture machinery oil " ,
2023-10-24 14:46:58 +00:00
p_set = p_set ,
2021-07-06 16:32:35 +00:00
)
2023-10-24 12:06:17 +00:00
n . madd (
" Link " ,
2023-10-24 14:46:58 +00:00
spatial . oil . agriculture_machinery ,
2023-10-24 12:06:17 +00:00
bus0 = spatial . oil . nodes ,
2023-10-24 14:46:58 +00:00
bus1 = spatial . oil . agriculture_machinery ,
2023-10-24 12:06:17 +00:00
bus2 = " co2 atmosphere " ,
carrier = " agriculture machinery oil " ,
p_nom_extendable = True ,
efficiency2 = costs . at [ " oil " , " CO2 intensity " ] ,
2021-07-06 16:32:35 +00:00
)
2019-05-16 14:08:16 +00:00
def decentral ( n ) :
2021-07-08 12:41:34 +00:00
"""
Removes the electricity transmission system .
"""
2021-07-01 18:09:04 +00:00
n . lines . drop ( n . lines . index , inplace = True )
n . links . drop ( n . links . index [ n . links . carrier . isin ( [ " DC " , " B2B " ] ) ] , inplace = True )
2019-05-16 14:08:16 +00:00
2019-07-12 06:51:25 +00:00
def remove_h2_network ( n ) :
2022-01-25 11:55:57 +00:00
n . links . drop (
n . links . index [ n . links . carrier . str . contains ( " H2 pipeline " ) ] , inplace = True
)
2019-07-12 06:51:25 +00:00
2021-07-01 18:09:04 +00:00
if " EU H2 Store " in n . stores . index :
n . stores . drop ( " EU H2 Store " , inplace = True )
2019-07-12 06:51:25 +00:00
2021-07-01 18:09:04 +00:00
def maybe_adjust_costs_and_potentials ( n , opts ) :
for o in opts :
if " + " not in o :
continue
oo = o . split ( " + " )
carrier_list = np . hstack (
2023-03-06 08:27:45 +00:00
(
2021-07-01 18:09:04 +00:00
n . generators . carrier . unique ( ) ,
n . links . carrier . unique ( ) ,
n . stores . carrier . unique ( ) ,
n . storage_units . carrier . unique ( ) ,
)
2023-03-06 08:27:45 +00:00
)
2021-07-01 18:09:04 +00:00
suptechs = map ( lambda c : c . split ( " - " , 2 ) [ 0 ] , carrier_list )
if oo [ 0 ] . startswith ( tuple ( suptechs ) ) :
carrier = oo [ 0 ]
2021-07-06 15:12:39 +00:00
attr_lookup = { " p " : " p_nom_max " , " e " : " e_nom_max " , " c " : " capital_cost " }
2021-07-01 18:09:04 +00:00
attr = attr_lookup [ oo [ 1 ] [ 0 ] ]
factor = float ( oo [ 1 ] [ 1 : ] )
# beware if factor is 0 and p_nom_max is np.inf, 0*np.inf is nan
if carrier == " AC " : # lines do not have carrier
n . lines [ attr ] * = factor
else :
2021-07-06 15:12:39 +00:00
if attr == " p_nom_max " :
comps = { " Generator " , " Link " , " StorageUnit " }
2021-08-04 16:13:01 +00:00
elif attr == " e_nom_max " :
2021-09-27 09:16:12 +00:00
comps = { " Store " }
2021-07-06 15:12:39 +00:00
else :
comps = { " Generator " , " Link " , " StorageUnit " , " Store " }
2021-07-01 18:09:04 +00:00
for c in n . iterate_components ( comps ) :
if carrier == " solar " :
sel = c . df . carrier . str . contains (
carrier
) & ~ c . df . carrier . str . contains ( " solar rooftop " )
else :
sel = c . df . carrier . str . contains ( carrier )
c . df . loc [ sel , attr ] * = factor
2023-02-16 17:42:19 +00:00
logger . info ( f " changing { attr } for { carrier } by factor { factor } " )
2019-07-12 06:51:25 +00:00
2021-07-01 18:09:04 +00:00
def limit_individual_line_extension ( n , maxext ) :
2023-02-24 13:42:51 +00:00
logger . info ( f " Limiting new HVAC and HVDC extensions to { maxext } MW " )
2021-07-01 18:09:04 +00:00
n . lines [ " s_nom_max " ] = n . lines [ " s_nom " ] + maxext
hvdc = n . links . index [ n . links . carrier == " DC " ]
n . links . loc [ hvdc , " p_nom_max " ] = n . links . loc [ hvdc , " p_nom " ] + maxext
2020-11-30 15:20:26 +00:00
2022-08-08 06:53:07 +00:00
2022-08-01 16:03:11 +00:00
aggregate_dict = {
" p_nom " : " sum " ,
" s_nom " : " sum " ,
" v_nom " : " max " ,
" v_mag_pu_max " : " min " ,
" v_mag_pu_min " : " max " ,
" p_nom_max " : " sum " ,
" s_nom_max " : " sum " ,
" p_nom_min " : " sum " ,
" s_nom_min " : " sum " ,
" v_ang_min " : " max " ,
" v_ang_max " : " min " ,
" terrain_factor " : " mean " ,
" num_parallel " : " sum " ,
" p_set " : " sum " ,
" e_initial " : " sum " ,
" e_nom " : " sum " ,
" e_nom_max " : " sum " ,
" e_nom_min " : " sum " ,
" state_of_charge_initial " : " sum " ,
" state_of_charge_set " : " sum " ,
" inflow " : " sum " ,
" p_max_pu " : " first " ,
" x " : " mean " ,
" y " : " mean " ,
}
2023-03-06 08:27:45 +00:00
2022-08-01 16:03:11 +00:00
def cluster_heat_buses ( n ) :
"""
Cluster residential and service heat buses to one representative bus .
2023-03-06 08:27:45 +00:00
2022-08-01 16:03:11 +00:00
This can be done to save memory and speed up optimisation
"""
def define_clustering ( attributes , aggregate_dict ) :
""" Define how attributes should be clustered.
Input :
attributes : pd . Index ( )
aggregate_dict : dictionary ( key : name of attribute , value
clustering method )
Returns :
agg : clustering dictionary
"""
keys = attributes . intersection ( aggregate_dict . keys ( ) )
agg = dict (
zip (
attributes . difference ( keys ) ,
[ " first " ] * len ( df . columns . difference ( keys ) ) ,
)
)
for key in keys :
agg [ key ] = aggregate_dict [ key ]
return agg
logger . info ( " Cluster residential and service heat buses. " )
components = [ " Bus " , " Carrier " , " Generator " , " Link " , " Load " , " Store " ]
for c in n . iterate_components ( components ) :
df = c . df
cols = df . columns [ df . columns . str . contains ( " bus " ) | ( df . columns == " carrier " ) ]
# rename columns and index
df [ cols ] = df [ cols ] . apply (
lambda x : x . str . replace ( " residential " , " " ) . str . replace ( " services " , " " ) ,
axis = 1 ,
)
df = df . rename (
index = lambda x : x . replace ( " residential " , " " ) . replace ( " services " , " " )
)
# cluster heat nodes
# static dataframe
agg = define_clustering ( df . columns , aggregate_dict )
df = df . groupby ( level = 0 ) . agg ( agg , * * agg_group_kwargs )
# time-varying data
pnl = c . pnl
agg = define_clustering ( pd . Index ( pnl . keys ( ) ) , aggregate_dict )
for k in pnl . keys ( ) :
pnl [ k ] . rename (
columns = lambda x : x . replace ( " residential " , " " ) . replace (
" services " , " "
) ,
inplace = True ,
)
pnl [ k ] = pnl [ k ] . groupby ( level = 0 , axis = 1 ) . agg ( agg [ k ] , * * agg_group_kwargs )
# remove unclustered assets of service/residential
to_drop = c . df . index . difference ( df . index )
n . mremove ( c . name , to_drop )
# add clustered assets
to_add = df . index . difference ( c . df . index )
import_components_from_dataframe ( n , df . loc [ to_add ] , c . name )
2022-08-08 06:53:07 +00:00
def apply_time_segmentation (
n , segments , solver_name = " cbc " , overwrite_time_dependent = True
2022-09-14 14:16:50 +00:00
) :
2022-08-08 06:53:07 +00:00
"""
Aggregating time series to segments with different lengths .
Input :
n : pypsa Network
segments : ( int ) number of segments in which the typical period should be
subdivided
solver_name : ( str ) name of solver
overwrite_time_dependent : ( bool ) overwrite time dependent data of pypsa network
with typical time series created by tsam
"""
try :
import tsam . timeseriesaggregation as tsam
except :
raise ModuleNotFoundError (
" Optional dependency ' tsam ' not found. " " Install via ' pip install tsam ' "
)
# get all time-dependent data
columns = pd . MultiIndex . from_tuples ( [ ] , names = [ " component " , " key " , " asset " ] )
raw = pd . DataFrame ( index = n . snapshots , columns = columns )
2022-09-14 14:16:50 +00:00
for c in n . iterate_components ( ) :
for attr , pnl in c . pnl . items ( ) :
# exclude e_min_pu which is used for SOC of EVs in the morning
if not pnl . empty and attr != " e_min_pu " :
df = pnl . copy ( )
df . columns = pd . MultiIndex . from_product ( [ [ c . name ] , [ attr ] , df . columns ] )
2022-08-08 06:53:07 +00:00
raw = pd . concat ( [ raw , df ] , axis = 1 )
# normalise all time-dependent data
annual_max = raw . max ( ) . replace ( 0 , 1 )
raw = raw . div ( annual_max , level = 0 )
# get representative segments
agg = tsam . TimeSeriesAggregation (
raw ,
hoursPerPeriod = len ( raw ) ,
noTypicalPeriods = 1 ,
noSegments = int ( segments ) ,
segmentation = True ,
solver = solver_name ,
)
segmented = agg . createTypicalPeriods ( )
weightings = segmented . index . get_level_values ( " Segment Duration " )
offsets = np . insert ( np . cumsum ( weightings [ : - 1 ] ) , 0 , 0 )
timesteps = [ raw . index [ 0 ] + pd . Timedelta ( f " { offset } h " ) for offset in offsets ]
snapshots = pd . DatetimeIndex ( timesteps )
sn_weightings = pd . Series (
weightings , index = snapshots , name = " weightings " , dtype = " float64 "
)
n . set_snapshots ( sn_weightings . index )
n . snapshot_weightings = n . snapshot_weightings . mul ( sn_weightings , axis = 0 )
# overwrite time-dependent data with timeseries created by tsam
if overwrite_time_dependent :
values_t = segmented . mul ( annual_max ) . set_index ( snapshots )
for component , key in values_t . columns . droplevel ( 2 ) . unique ( ) :
n . pnl ( component ) [ key ] = values_t [ component , key ]
return n
2023-03-06 08:27:45 +00:00
2022-08-08 06:53:07 +00:00
def set_temporal_aggregation ( n , opts , solver_name ) :
"""
Aggregate network temporally .
"""
for o in opts :
# temporal averaging
m = re . match ( r " ^ \ d+h$ " , o , re . IGNORECASE )
if m is not None :
n = average_every_nhours ( n , m . group ( 0 ) )
2022-09-14 14:16:50 +00:00
break
2022-11-18 08:08:07 +00:00
# representative snapshots
2022-09-14 14:16:50 +00:00
m = re . match ( r " (^ \ d+)sn$ " , o , re . IGNORECASE )
2022-08-08 06:53:07 +00:00
if m is not None :
2022-09-14 14:16:50 +00:00
sn = int ( m [ 1 ] )
2023-02-24 13:42:51 +00:00
logger . info ( f " Use every { sn } snapshot as representative " )
2022-08-08 06:53:07 +00:00
n . set_snapshots ( n . snapshots [ : : sn ] )
n . snapshot_weightings * = sn
2022-09-14 14:16:50 +00:00
break
2022-08-08 06:53:07 +00:00
# segments with package tsam
2022-09-14 14:16:50 +00:00
m = re . match ( r " ^( \ d+)seg$ " , o , re . IGNORECASE )
if m is not None :
segments = int ( m [ 1 ] )
2023-02-24 13:42:51 +00:00
logger . info ( f " Use temporal segmentation with { segments } segments " )
2022-08-08 06:53:07 +00:00
n = apply_time_segmentation ( n , segments , solver_name = solver_name )
2022-09-14 14:16:50 +00:00
break
2022-08-08 06:53:07 +00:00
return n
2023-01-30 10:53:00 +00:00
2023-03-06 08:27:45 +00:00
2019-04-17 12:24:22 +00:00
if __name__ == " __main__ " :
if " snakemake " not in globals ( ) :
2023-03-06 18:09:45 +00:00
from _helpers import mock_snakemake
2023-03-06 08:27:45 +00:00
2021-07-01 18:09:04 +00:00
snakemake = mock_snakemake (
" prepare_sector_network " ,
2023-03-10 13:12:22 +00:00
configfiles = " test/config.overnight.yaml " ,
2021-07-01 18:09:04 +00:00
simpl = " " ,
2021-07-08 12:41:34 +00:00
opts = " " ,
2023-03-10 13:12:22 +00:00
clusters = " 5 " ,
2023-03-09 07:36:41 +00:00
ll = " v1.5 " ,
2023-03-10 13:12:22 +00:00
sector_opts = " CO2L0-24H-T-H-B-I-A-solar+p3-dist1 " ,
planning_horizons = " 2030 " ,
2020-07-29 13:50:40 +00:00
)
2019-04-17 12:24:22 +00:00
2023-03-06 13:27:15 +00:00
logging . basicConfig ( level = snakemake . config [ " logging " ] [ " level " ] )
2019-04-17 12:24:22 +00:00
2022-07-20 09:35:12 +00:00
update_config_with_sector_opts ( snakemake . config , snakemake . wildcards . sector_opts )
2023-06-15 16:52:25 +00:00
options = snakemake . params . sector
2019-04-17 12:24:22 +00:00
2019-04-17 15:04:33 +00:00
opts = snakemake . wildcards . sector_opts . split ( " - " )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
investment_year = int ( snakemake . wildcards . planning_horizons [ - 4 : ] )
2019-04-17 12:24:22 +00:00
2023-07-13 20:31:55 +00:00
n = pypsa . Network ( snakemake . input . network )
2020-07-14 11:28:10 +00:00
2021-07-01 18:09:04 +00:00
pop_layout = pd . read_csv ( snakemake . input . clustered_pop_layout , index_col = 0 )
2023-03-10 14:58:53 +00:00
nhours = n . snapshot_weightings . generators . sum ( )
nyears = nhours / 8760
2020-09-25 13:25:41 +00:00
2020-07-29 13:50:40 +00:00
costs = prepare_costs (
snakemake . input . costs ,
2023-06-15 16:52:25 +00:00
snakemake . params . costs ,
2023-03-10 13:12:22 +00:00
nyears ,
2020-11-30 16:01:14 +00:00
)
2019-04-17 12:24:22 +00:00
2023-03-10 13:12:22 +00:00
pop_weighted_energy_totals = (
pd . read_csv ( snakemake . input . pop_weighted_energy_totals , index_col = 0 ) * nyears
2022-04-03 16:49:35 +00:00
)
2021-07-01 18:09:04 +00:00
patch_electricity_network ( n )
2019-04-18 13:23:37 +00:00
2022-03-18 12:46:40 +00:00
spatial = define_spatial ( pop_layout . index , options )
2021-07-12 10:37:37 +00:00
2023-08-23 11:24:25 +00:00
if snakemake . params . foresight in [ " myopic " , " perfect " ] :
2021-07-01 18:09:04 +00:00
add_lifetime_wind_solar ( n , costs )
2019-07-16 14:00:21 +00:00
2023-06-15 16:52:25 +00:00
conventional = snakemake . params . conventional_carriers
2021-07-02 13:00:19 +00:00
for carrier in conventional :
add_carrier_buses ( n , carrier )
2020-12-02 12:03:13 +00:00
2021-07-01 18:09:04 +00:00
add_co2_tracking ( n , options )
2020-07-18 09:22:30 +00:00
2021-07-01 18:09:04 +00:00
add_generation ( n , costs )
2020-08-19 10:41:17 +00:00
2021-11-03 19:34:43 +00:00
add_storage_and_grids ( n , costs )
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
# TODO merge with opts cost adjustment below
2019-07-31 12:22:33 +00:00
for o in opts :
2020-01-14 20:36:35 +00:00
if o [ : 4 ] == " wave " :
2021-07-01 18:09:04 +00:00
wave_cost_factor = float ( o [ 4 : ] . replace ( " p " , " . " ) . replace ( " m " , " - " ) )
2023-02-16 17:42:19 +00:00
logger . info (
f " Including wave generators with cost factor of { wave_cost_factor } "
)
2020-01-14 20:36:35 +00:00
add_wave ( n , wave_cost_factor )
2020-03-25 11:52:25 +00:00
if o [ : 4 ] == " dist " :
2021-07-01 18:09:04 +00:00
options [ " electricity_distribution_grid " ] = True
options [ " electricity_distribution_grid_cost_factor " ] = float (
o [ 4 : ] . replace ( " p " , " . " ) . replace ( " m " , " - " )
2023-03-06 08:27:45 +00:00
)
2020-10-21 05:21:09 +00:00
if o == " biomasstransport " :
options [ " biomass_transport " ] = True
2019-04-17 12:24:22 +00:00
2019-07-16 14:00:21 +00:00
if " nodistrict " in opts :
2021-09-29 12:37:36 +00:00
options [ " district_heating " ] [ " progress " ] = 0.0
2019-07-16 14:00:21 +00:00
2019-04-17 12:24:22 +00:00
if " T " in opts :
2021-07-01 18:09:04 +00:00
add_land_transport ( n , costs )
2019-04-17 12:24:22 +00:00
if " H " in opts :
2021-07-01 18:09:04 +00:00
add_heat ( n , costs )
2019-04-17 12:24:22 +00:00
if " B " in opts :
2021-07-01 18:09:04 +00:00
add_biomass ( n , costs )
2019-04-17 12:24:22 +00:00
2023-02-16 19:13:26 +00:00
if options [ " ammonia " ] :
add_ammonia ( n , costs )
2019-04-17 12:24:22 +00:00
if " I " in opts :
2021-07-01 18:09:04 +00:00
add_industry ( n , costs )
2019-04-17 12:24:22 +00:00
2019-05-14 09:49:32 +00:00
if " I " in opts and " H " in opts :
add_waste_heat ( n )
2021-07-06 16:32:35 +00:00
if " A " in opts : # requires H and I
add_agriculture ( n , costs )
2020-12-09 17:19:57 +00:00
if options [ " dac " ] :
2021-07-01 18:09:04 +00:00
add_dac ( n , costs )
2020-12-09 17:19:57 +00:00
2019-05-16 14:08:16 +00:00
if " decentral " in opts :
decentral ( n )
2019-07-12 06:51:25 +00:00
if " noH2network " in opts :
remove_h2_network ( n )
2023-01-24 17:44:39 +00:00
if options [ " co2network " ] :
2021-07-07 16:07:57 +00:00
add_co2_network ( n , costs )
2023-02-16 16:21:58 +00:00
if options [ " allam_cycle " ] :
2023-02-08 21:57:01 +00:00
add_allam ( n , costs )
2021-07-07 16:07:57 +00:00
2022-08-08 06:53:07 +00:00
solver_name = snakemake . config [ " solving " ] [ " solver " ] [ " name " ]
n = set_temporal_aggregation ( n , opts , solver_name )
2020-07-29 13:50:40 +00:00
2021-07-01 18:09:04 +00:00
limit_type = " config "
2023-06-15 16:52:25 +00:00
limit = get ( snakemake . params . co2_budget , investment_year )
2020-12-30 14:55:08 +00:00
for o in opts :
2023-03-07 16:21:00 +00:00
if " cb " not in o :
2021-07-01 18:09:04 +00:00
continue
limit_type = " carbon budget "
2023-03-06 15:54:35 +00:00
fn = " results/ " + snakemake . params . RDIR + " /csvs/carbon_budget_distribution.csv "
2021-07-01 18:09:04 +00:00
if not os . path . exists ( fn ) :
2023-06-15 16:52:25 +00:00
emissions_scope = snakemake . params . emissions_scope
2023-06-15 17:12:30 +00:00
report_year = snakemake . params . eurostat_report_year
2023-08-09 12:35:39 +00:00
input_co2 = snakemake . input . co2
2022-08-02 06:52:48 +00:00
build_carbon_budget (
2023-08-22 12:49:44 +00:00
o ,
snakemake . input . eurostat ,
fn ,
emissions_scope ,
report_year ,
input_co2 ,
2022-08-02 06:52:48 +00:00
)
2022-04-12 12:37:05 +00:00
co2_cap = pd . read_csv ( fn , index_col = 0 ) . squeeze ( )
2022-08-01 13:21:11 +00:00
limit = co2_cap . loc [ investment_year ]
2021-07-01 18:09:04 +00:00
break
2020-11-30 12:21:38 +00:00
for o in opts :
2023-03-07 16:21:00 +00:00
if " Co2L " not in o :
2021-07-01 18:09:04 +00:00
continue
limit_type = " wildcard "
limit = o [ o . find ( " Co2L " ) + 4 : ]
limit = float ( limit . replace ( " p " , " . " ) . replace ( " m " , " - " ) )
break
2023-02-16 17:42:19 +00:00
logger . info ( f " Add CO2 limit from { limit_type } " )
2023-03-10 13:12:22 +00:00
add_co2limit ( n , nyears , limit )
2019-04-17 12:24:22 +00:00
2020-10-28 17:21:28 +00:00
for o in opts :
2021-07-01 18:09:04 +00:00
if not o [ : 10 ] == " linemaxext " :
continue
maxext = float ( o [ 10 : ] ) * 1e3
limit_individual_line_extension ( n , maxext )
break
2019-04-17 12:24:22 +00:00
2021-07-01 18:09:04 +00:00
if options [ " electricity_distribution_grid " ] :
insert_electricity_distribution_grid ( n , costs )
2020-10-28 17:21:28 +00:00
2021-07-01 18:09:04 +00:00
maybe_adjust_costs_and_potentials ( n , opts )
2020-10-28 17:21:28 +00:00
2021-07-01 18:09:04 +00:00
if options [ " gas_distribution_grid " ] :
insert_gas_distribution_costs ( n , costs )
if options [ " electricity_grid_connection " ] :
add_electricity_grid_connection ( n , costs )
2020-07-29 13:50:40 +00:00
2023-08-23 11:24:25 +00:00
first_year_myopic = ( snakemake . params . foresight in [ " myopic " , " perfect " ] ) and (
2023-06-15 16:52:25 +00:00
snakemake . params . planning_horizons [ 0 ] == investment_year
2022-08-01 16:15:35 +00:00
)
2023-01-30 10:54:19 +00:00
if options . get ( " cluster_heat_buses " , False ) and not first_year_myopic :
2022-08-01 16:03:11 +00:00
cluster_heat_buses ( n )
2022-08-01 16:15:35 +00:00
2022-06-30 06:42:18 +00:00
n . meta = dict ( snakemake . config , * * dict ( wildcards = dict ( snakemake . wildcards ) ) )
2023-01-30 10:53:00 +00:00
2023-05-03 11:24:57 +00:00
sanitize_carriers ( n , snakemake . config )
2023-01-30 10:53:00 +00:00
2019-04-17 12:24:22 +00:00
n . export_to_netcdf ( snakemake . output [ 0 ] )