2022-09-16 13:04:04 +00:00
# -*- coding: utf-8 -*-
2023-03-10 15:00:52 +00:00
# SPDX-FileCopyrightText: : 2017-2023 The PyPSA-Eur Authors
2020-05-29 07:50:55 +00:00
#
2021-09-14 14:37:41 +00:00
# SPDX-License-Identifier: MIT
2019-08-08 13:02:28 +00:00
"""
2023-03-10 15:07:00 +00:00
Solves optimal operation and capacity for a network with the option to
iteratively optimize while updating line reactances .
This script is used for optimizing the electrical network as well as the
sector coupled network .
Description
- - - - - - - - - - -
2023-03-10 15:10:01 +00:00
2023-03-10 15:07:00 +00:00
Total annual system costs are minimised with PyPSA . The full formulation of the
linear optimal power flow ( plus investment planning
is provided in the
` documentation of PyPSA < https : / / pypsa . readthedocs . io / en / latest / optimal_power_flow . html #linear-optimal-power-flow>`_.
2023-03-10 15:10:01 +00:00
2023-03-15 16:00:06 +00:00
The optimization is based on the : func : ` network . optimize ` function .
Additionally , some extra constraints specified in : mod : ` solve_network ` are added .
2023-03-10 15:07:00 +00:00
2023-03-15 16:00:06 +00:00
. . note : :
2023-03-10 15:10:01 +00:00
2023-03-15 16:00:06 +00:00
The rules ` ` solve_elec_networks ` ` and ` ` solve_sector_networks ` ` run
the workflow for all scenarios in the configuration file ( ` ` scenario : ` ` )
based on the rule : mod : ` solve_network ` .
2023-03-10 15:07:00 +00:00
"""
2023-03-09 21:51:56 +00:00
import logging
import re
2019-08-11 11:17:36 +00:00
2023-03-09 21:51:56 +00:00
import numpy as np
import pandas as pd
import pypsa
import xarray as xr
2023-08-30 12:33:23 +00:00
from _benchmark import memory_logger
2023-07-13 20:35:03 +00:00
from _helpers import configure_logging , update_config_with_sector_opts
2023-08-30 10:05:16 +00:00
from pypsa . descriptors import get_activity_mask
2023-08-24 13:48:06 +00:00
2023-03-09 21:51:56 +00:00
logger = logging . getLogger ( __name__ )
pypsa . pf . logger . setLevel ( logging . WARNING )
2023-04-20 07:35:53 +00:00
from pypsa . descriptors import get_switchable_as_dense as get_as_dense
2023-08-30 09:50:49 +00:00
2023-10-18 14:59:49 +00:00
from prepare_sector_network import emission_sectors_from_opts
2019-08-11 09:40:47 +00:00
2023-06-15 16:52:25 +00:00
def add_land_use_constraint ( n , planning_horizons , config ) :
2023-03-09 21:51:56 +00:00
if " m " in snakemake . wildcards . clusters :
2023-06-15 16:52:25 +00:00
_add_land_use_constraint_m ( n , planning_horizons , config )
2023-03-09 21:51:56 +00:00
else :
2023-06-02 10:52:49 +00:00
_add_land_use_constraint ( n )
2023-08-30 10:05:16 +00:00
2023-08-25 08:49:03 +00:00
def add_land_use_constraint_perfect ( n ) :
2023-08-30 10:05:16 +00:00
"""
Add global constraints for tech capacity limit .
2023-08-25 08:49:03 +00:00
"""
logger . info ( " Add land-use constraint for perfect foresight " )
2023-08-30 10:05:16 +00:00
2023-08-25 08:49:03 +00:00
def compress_series ( s ) :
def process_group ( group ) :
if group . nunique ( ) == 1 :
return pd . Series ( group . iloc [ 0 ] , index = [ None ] )
else :
return group
2023-08-30 10:05:16 +00:00
return s . groupby ( level = [ 0 , 1 ] ) . apply ( process_group )
2023-08-25 08:49:03 +00:00
def new_index_name ( t ) :
# Convert all elements to string and filter out None values
parts = [ str ( x ) for x in t if x is not None ]
# Join with space, but use a dash for the last item if not None
2023-08-30 10:05:16 +00:00
return " " . join ( parts [ : 2 ] ) + ( f " - { parts [ - 1 ] } " if len ( parts ) > 2 else " " )
2023-08-25 08:49:03 +00:00
def check_p_min_p_max ( p_nom_max ) :
p_nom_min = n . generators [ ext_i ] . groupby ( grouper ) . sum ( ) . p_nom_min
p_nom_min = p_nom_min . reindex ( p_nom_max . index )
2023-08-30 10:05:16 +00:00
check = (
p_nom_min . groupby ( level = [ 0 , 1 ] ) . sum ( )
> p_nom_max . groupby ( level = [ 0 , 1 ] ) . min ( )
)
2023-08-25 08:49:03 +00:00
if check . sum ( ) :
2023-08-30 10:05:16 +00:00
logger . warning (
f " summed p_min_pu values at node larger than technical potential { check [ check ] . index } "
)
2023-08-25 08:49:03 +00:00
grouper = [ n . generators . carrier , n . generators . bus , n . generators . build_year ]
ext_i = n . generators . p_nom_extendable
# get technical limit per node and investment period
p_nom_max = n . generators [ ext_i ] . groupby ( grouper ) . min ( ) . p_nom_max
# drop carriers without tech limit
p_nom_max = p_nom_max [ ~ p_nom_max . isin ( [ np . inf , np . nan ] ) ]
# carrier
carriers = p_nom_max . index . get_level_values ( 0 ) . unique ( )
2023-08-30 10:05:16 +00:00
gen_i = n . generators [ ( n . generators . carrier . isin ( carriers ) ) & ( ext_i ) ] . index
2023-08-25 08:49:03 +00:00
n . generators . loc [ gen_i , " p_nom_min " ] = 0
# check minimum capacities
check_p_min_p_max ( p_nom_max )
# drop multi entries in case p_nom_max stays constant in different periods
# p_nom_max = compress_series(p_nom_max)
# adjust name to fit syntax of nominal constraint per bus
df = p_nom_max . reset_index ( )
2023-08-30 10:05:16 +00:00
df [ " name " ] = df . apply (
lambda row : f " nom_max_ { row [ ' carrier ' ] } "
+ ( f " _ { row [ ' build_year ' ] } " if row [ " build_year " ] is not None else " " ) ,
axis = 1 ,
)
2023-08-25 08:49:03 +00:00
for name in df . name . unique ( ) :
2023-08-30 10:05:16 +00:00
df_carrier = df [ df . name == name ]
2023-08-25 08:49:03 +00:00
bus = df_carrier . bus
n . buses . loc [ bus , name ] = df_carrier . p_nom_max . values
2023-08-30 10:05:16 +00:00
2023-08-25 08:49:03 +00:00
return n
2019-08-11 20:34:18 +00:00
2019-08-11 09:40:47 +00:00
2023-06-02 10:52:49 +00:00
def _add_land_use_constraint ( n ) :
2023-03-09 21:51:56 +00:00
# warning: this will miss existing offwind which is not classed AC-DC and has carrier 'offwind'
2019-08-11 20:34:18 +00:00
2023-03-09 21:51:56 +00:00
for carrier in [ " solar " , " onwind " , " offwind-ac " , " offwind-dc " ] :
2023-08-22 06:31:46 +00:00
extendable_i = ( n . generators . carrier == carrier ) & n . generators . p_nom_extendable
n . generators . loc [ extendable_i , " p_nom_min " ] = 0
2023-08-22 06:41:41 +00:00
2023-03-09 21:51:56 +00:00
ext_i = ( n . generators . carrier == carrier ) & ~ n . generators . p_nom_extendable
existing = (
n . generators . loc [ ext_i , " p_nom " ]
. groupby ( n . generators . bus . map ( n . buses . location ) )
. sum ( )
)
existing . index + = " " + carrier + " - " + snakemake . wildcards . planning_horizons
n . generators . loc [ existing . index , " p_nom_max " ] - = existing
# check if existing capacities are larger than technical potential
existing_large = n . generators [
n . generators [ " p_nom_min " ] > n . generators [ " p_nom_max " ]
] . index
if len ( existing_large ) :
logger . warning (
f " Existing capacities larger than technical potential for { existing_large } , \
2023-08-22 09:08:51 +00:00
adjust technical potential to existing capacities "
2023-03-09 21:51:56 +00:00
)
n . generators . loc [ existing_large , " p_nom_max " ] = n . generators . loc [
existing_large , " p_nom_min "
]
2019-08-14 08:35:41 +00:00
2023-03-09 21:51:56 +00:00
n . generators . p_nom_max . clip ( lower = 0 , inplace = True )
2019-08-11 09:40:47 +00:00
2019-08-14 09:07:52 +00:00
2023-06-15 16:52:25 +00:00
def _add_land_use_constraint_m ( n , planning_horizons , config ) :
2023-03-09 21:51:56 +00:00
# if generators clustering is lower than network clustering, land_use accounting is at generators clusters
2019-08-14 09:07:52 +00:00
2023-03-09 21:51:56 +00:00
grouping_years = config [ " existing_capacities " ] [ " grouping_years " ]
current_horizon = snakemake . wildcards . planning_horizons
2019-08-14 09:07:52 +00:00
2023-03-09 21:51:56 +00:00
for carrier in [ " solar " , " onwind " , " offwind-ac " , " offwind-dc " ] :
existing = n . generators . loc [ n . generators . carrier == carrier , " p_nom " ]
ind = list (
2023-10-08 09:20:57 +00:00
{ i . split ( sep = " " ) [ 0 ] + " " + i . split ( sep = " " ) [ 1 ] for i in existing . index }
2023-03-09 21:51:56 +00:00
)
2019-08-14 09:07:52 +00:00
2023-03-09 21:51:56 +00:00
previous_years = [
str ( y )
for y in planning_horizons + grouping_years
if y < int ( snakemake . wildcards . planning_horizons )
]
2019-08-14 09:07:52 +00:00
2023-03-09 21:51:56 +00:00
for p_year in previous_years :
ind2 = [
i for i in ind if i + " " + carrier + " - " + p_year in existing . index
]
sel_current = [ i + " " + carrier + " - " + current_horizon for i in ind2 ]
sel_p_year = [ i + " " + carrier + " - " + p_year for i in ind2 ]
n . generators . loc [ sel_current , " p_nom_max " ] - = existing . loc [
sel_p_year
] . rename ( lambda x : x [ : - 4 ] + current_horizon )
2019-08-14 09:07:52 +00:00
2023-03-09 21:51:56 +00:00
n . generators . p_nom_max . clip ( lower = 0 , inplace = True )
2019-08-08 13:02:28 +00:00
2019-11-28 07:22:52 +00:00
2023-08-24 06:42:24 +00:00
def add_co2_sequestration_limit ( n , config , limit = 200 ) :
2023-03-09 21:51:56 +00:00
"""
Add a global constraint on the amount of Mt CO2 that can be sequestered .
"""
n . carriers . loc [ " co2 stored " , " co2_absorptions " ] = - 1
n . carriers . co2_absorptions = n . carriers . co2_absorptions . fillna ( 0 )
2018-02-01 11:42:56 +00:00
2023-03-09 21:51:56 +00:00
limit = limit * 1e6
for o in opts :
if " seq " not in o :
continue
limit = float ( o [ o . find ( " seq " ) + 3 : ] ) * 1e6
break
2023-08-30 10:05:16 +00:00
2023-09-30 06:14:54 +00:00
if not n . investment_periods . empty :
2023-08-24 06:42:24 +00:00
periods = n . investment_periods
names = pd . Index ( [ f " co2_sequestration_limit- { period } " for period in periods ] )
else :
periods = [ np . nan ]
names = pd . Index ( [ " co2_sequestration_limit " ] )
2023-08-30 10:05:16 +00:00
2023-08-24 06:42:24 +00:00
n . madd (
2023-03-09 21:51:56 +00:00
" GlobalConstraint " ,
2023-08-24 06:42:24 +00:00
names ,
2023-03-09 21:51:56 +00:00
sense = " <= " ,
constant = limit ,
type = " primary_energy " ,
carrier_attribute = " co2_absorptions " ,
2023-08-24 06:42:24 +00:00
investment_period = periods ,
2023-03-09 21:51:56 +00:00
)
2020-12-03 18:50:53 +00:00
2023-08-25 13:50:26 +00:00
def add_carbon_constraint ( n , snapshots ) :
2023-08-24 06:42:24 +00:00
glcs = n . global_constraints . query ( ' type == " co2_limit " ' )
if glcs . empty :
return
for name , glc in glcs . iterrows ( ) :
carattr = glc . carrier_attribute
emissions = n . carriers . query ( f " { carattr } != 0 " ) [ carattr ]
if emissions . empty :
continue
# stores
n . stores [ " carrier " ] = n . stores . bus . map ( n . buses . carrier )
stores = n . stores . query ( " carrier in @emissions.index and not e_cyclic " )
if not stores . empty :
last = n . snapshot_weightings . reset_index ( ) . groupby ( " period " ) . last ( )
2023-08-30 10:05:16 +00:00
last_i = last . set_index ( [ last . index , last . timestep ] ) . index
2023-08-24 06:42:24 +00:00
final_e = n . model [ " Store-e " ] . loc [ last_i , stores . index ]
2023-10-08 09:20:36 +00:00
time_valid = int ( glc . loc [ " investment_period " ] )
2023-08-24 06:42:24 +00:00
time_i = pd . IndexSlice [ time_valid , : ]
2023-08-30 10:05:16 +00:00
lhs = final_e . loc [ time_i , : ] - final_e . shift ( snapshot = 1 ) . loc [ time_i , : ]
2023-10-08 09:20:36 +00:00
rhs = glc . constant
2023-08-24 06:42:24 +00:00
n . model . add_constraints ( lhs < = rhs , name = f " GlobalConstraint- { name } " )
2023-08-25 09:56:29 +00:00
2023-08-25 13:50:26 +00:00
def add_carbon_budget_constraint ( n , snapshots ) :
glcs = n . global_constraints . query ( ' type == " Co2Budget " ' )
if glcs . empty :
return
for name , glc in glcs . iterrows ( ) :
carattr = glc . carrier_attribute
emissions = n . carriers . query ( f " { carattr } != 0 " ) [ carattr ]
if emissions . empty :
continue
# stores
n . stores [ " carrier " ] = n . stores . bus . map ( n . buses . carrier )
stores = n . stores . query ( " carrier in @emissions.index and not e_cyclic " )
if not stores . empty :
last = n . snapshot_weightings . reset_index ( ) . groupby ( " period " ) . last ( )
2023-08-30 10:05:16 +00:00
last_i = last . set_index ( [ last . index , last . timestep ] ) . index
2023-08-25 13:50:26 +00:00
final_e = n . model [ " Store-e " ] . loc [ last_i , stores . index ]
2023-10-08 09:20:36 +00:00
time_valid = int ( glc . loc [ " investment_period " ] )
2023-08-25 13:50:26 +00:00
time_i = pd . IndexSlice [ time_valid , : ]
2023-10-08 09:20:36 +00:00
weighting = n . investment_period_weightings . loc [ time_valid , " years " ]
2023-08-30 10:05:16 +00:00
lhs = final_e . loc [ time_i , : ] * weighting
2023-10-08 09:20:36 +00:00
rhs = glc . constant
2023-08-25 13:50:26 +00:00
n . model . add_constraints ( lhs < = rhs , name = f " GlobalConstraint- { name } " )
2023-08-25 11:27:34 +00:00
def add_max_growth ( n , config ) :
"""
2023-08-30 10:05:16 +00:00
Add maximum growth rates for different carriers .
"""
2023-08-30 13:57:47 +00:00
opts = snakemake . params [ " sector " ] [ " limit_max_growth " ]
2023-08-25 11:27:34 +00:00
# take maximum yearly difference between investment periods since historic growth is per year
factor = n . investment_period_weightings . years . max ( ) * opts [ " factor " ]
for carrier in opts [ " max_growth " ] . keys ( ) :
max_per_period = opts [ " max_growth " ] [ carrier ] * factor
2023-08-30 10:05:16 +00:00
logger . info (
f " set maximum growth rate per investment period of { carrier } to { max_per_period } GW. "
)
2023-08-25 11:27:34 +00:00
n . carriers . loc [ carrier , " max_growth " ] = max_per_period * 1e3
2023-08-30 10:05:16 +00:00
2023-08-25 11:27:34 +00:00
for carrier in opts [ " max_relative_growth " ] . keys ( ) :
max_r_per_period = opts [ " max_relative_growth " ] [ carrier ]
2023-08-30 10:05:16 +00:00
logger . info (
f " set maximum relative growth per investment period of { carrier } to { max_r_per_period } . "
)
n . carriers . loc [ carrier , " max_relative_growth " ] = max_r_per_period
2023-08-25 09:56:29 +00:00
return n
2023-08-28 11:31:02 +00:00
def add_retrofit_gas_boiler_constraint ( n , snapshots ) :
2023-08-30 10:05:16 +00:00
"""
Allow retrofitting of existing gas boilers to H2 boilers .
2023-08-28 11:31:02 +00:00
"""
c = " Link "
logger . info ( " Add constraint for retrofitting gas boilers to H2 boilers. " )
# existing gas boilers
mask = n . links . carrier . str . contains ( " gas boiler " ) & ~ n . links . p_nom_extendable
gas_i = n . links [ mask ] . index
2023-08-30 10:05:16 +00:00
mask = n . links . carrier . str . contains ( " retrofitted H2 boiler " )
2023-08-28 11:31:02 +00:00
h2_i = n . links [ mask ] . index
2023-08-30 10:05:16 +00:00
2023-08-28 11:31:02 +00:00
n . links . loc [ gas_i , " p_nom_extendable " ] = True
p_nom = n . links . loc [ gas_i , " p_nom " ]
n . links . loc [ gas_i , " p_nom " ] = 0
2023-08-30 10:05:16 +00:00
2023-08-28 11:31:02 +00:00
# heat profile
2023-08-30 10:05:16 +00:00
cols = n . loads_t . p_set . columns [
n . loads_t . p_set . columns . str . contains ( " heat " )
& ~ n . loads_t . p_set . columns . str . contains ( " industry " )
& ~ n . loads_t . p_set . columns . str . contains ( " agriculture " )
]
profile = n . loads_t . p_set [ cols ] . div (
n . loads_t . p_set [ cols ] . groupby ( level = 0 ) . max ( ) , level = 0
)
2023-08-28 11:31:02 +00:00
# to deal if max value is zero
profile . fillna ( 0 , inplace = True )
profile . rename ( columns = n . loads . bus . to_dict ( ) , inplace = True )
profile = profile . reindex ( columns = n . links . loc [ gas_i , " bus1 " ] )
profile . columns = gas_i
2023-08-30 10:05:16 +00:00
2023-08-28 11:31:02 +00:00
rhs = profile . mul ( p_nom )
2023-08-30 10:05:16 +00:00
2023-08-28 11:31:02 +00:00
dispatch = n . model [ " Link-p " ]
active = get_activity_mask ( n , c , snapshots , gas_i )
rhs = rhs [ active ]
p_gas = dispatch . sel ( Link = gas_i )
p_h2 = dispatch . sel ( Link = h2_i )
2023-08-30 10:05:16 +00:00
2023-08-28 11:31:02 +00:00
lhs = p_gas + p_h2
2023-08-30 10:05:16 +00:00
2023-08-28 11:31:02 +00:00
n . model . add_constraints ( lhs == rhs , name = " gas_retrofit " )
2023-08-30 10:05:16 +00:00
2023-06-02 10:53:09 +00:00
def prepare_network (
n ,
solve_opts = None ,
config = None ,
2023-06-15 16:52:25 +00:00
foresight = None ,
planning_horizons = None ,
co2_sequestration_potential = None ,
2023-06-02 10:53:09 +00:00
) :
2022-09-16 13:04:04 +00:00
if " clip_p_max_pu " in solve_opts :
2023-03-09 21:51:56 +00:00
for df in (
n . generators_t . p_max_pu ,
2023-07-10 23:02:55 +00:00
n . generators_t . p_min_pu ,
2023-03-09 21:51:56 +00:00
n . storage_units_t . inflow ,
) :
2022-09-16 13:04:04 +00:00
df . where ( df > solve_opts [ " clip_p_max_pu " ] , other = 0.0 , inplace = True )
2017-12-18 19:31:27 +00:00
2023-10-08 09:20:36 +00:00
if load_shedding := solve_opts . get ( " load_shedding " ) :
2023-03-09 21:51:56 +00:00
# intersect between macroeconomic and surveybased willingness to pay
# http://journal.frontiersin.org/article/10.3389/fenrg.2015.00055/full
# TODO: retrieve color and nice name from config
2022-03-03 22:08:29 +00:00
n . add ( " Carrier " , " load " , color = " #dd2e23 " , nice_name = " Load shedding " )
2023-11-24 09:00:07 +00:00
buses_i = n . buses . index
2022-09-16 13:04:04 +00:00
if not np . isscalar ( load_shedding ) :
2023-03-09 21:51:56 +00:00
# TODO: do not scale via sign attribute (use Eur/MWh instead of Eur/kWh)
2022-09-16 13:04:04 +00:00
load_shedding = 1e2 # Eur/kWh
2023-03-09 21:51:56 +00:00
2022-09-16 13:04:04 +00:00
n . madd (
" Generator " ,
buses_i ,
" load " ,
2023-04-26 16:05:56 +00:00
bus = buses_i ,
2022-09-16 13:04:04 +00:00
carrier = " load " ,
sign = 1e-3 , # Adjust sign to measure p and p_nom in kW instead of MW
2023-03-09 21:51:56 +00:00
marginal_cost = load_shedding , # Eur/kWh
2022-09-16 13:04:04 +00:00
p_nom = 1e9 , # kW
)
if solve_opts . get ( " noisy_costs " ) :
2023-03-09 21:51:56 +00:00
for t in n . iterate_components ( ) :
2022-09-16 13:04:04 +00:00
# if 'capital_cost' in t.df:
2017-12-18 19:31:27 +00:00
# t.df['capital_cost'] += 1e1 + 2.*(np.random.random(len(t.df)) - 0.5)
2022-09-16 13:04:04 +00:00
if " marginal_cost " in t . df :
t . df [ " marginal_cost " ] + = 1e-2 + 2e-3 * (
np . random . random ( len ( t . df ) ) - 0.5
)
for t in n . iterate_components ( [ " Line " , " Link " ] ) :
t . df [ " capital_cost " ] + = (
1e-1 + 2e-2 * ( np . random . random ( len ( t . df ) ) - 0.5 )
) * t . df [ " length " ]
if solve_opts . get ( " nhours " ) :
nhours = solve_opts [ " nhours " ]
2018-03-09 14:41:46 +00:00
n . set_snapshots ( n . snapshots [ : nhours ] )
2022-09-16 13:04:04 +00:00
n . snapshot_weightings [ : ] = 8760.0 / nhours
2017-12-18 19:31:27 +00:00
2023-06-15 16:52:25 +00:00
if foresight == " myopic " :
add_land_use_constraint ( n , planning_horizons , config )
2023-08-30 10:05:16 +00:00
2023-08-25 08:49:03 +00:00
if foresight == " perfect " :
n = add_land_use_constraint_perfect ( n )
2023-08-30 13:57:47 +00:00
if snakemake . params [ " sector " ] [ " limit_max_growth " ] [ " enable " ] :
2023-08-25 11:27:34 +00:00
n = add_max_growth ( n , config )
2023-03-09 21:51:56 +00:00
if n . stores . carrier . eq ( " co2 stored " ) . any ( ) :
2023-06-15 16:52:25 +00:00
limit = co2_sequestration_potential
2023-08-24 06:42:24 +00:00
add_co2_sequestration_limit ( n , config , limit = limit )
2023-03-09 21:51:56 +00:00
2017-12-18 19:31:27 +00:00
return n
2018-02-10 16:19:46 +00:00
2020-02-10 11:06:43 +00:00
def add_CCL_constraints ( n , config ) :
2023-03-09 13:24:25 +00:00
"""
Add CCL ( country & carrier limit ) constraint to the network .
Add minimum and maximum levels of generator nominal capacity per carrier
for individual countries . Opts and path for agg_p_nom_minmax . csv must be defined
in config . yaml . Default file is available at data / agg_p_nom_minmax . csv .
Parameters
- - - - - - - - - -
n : pypsa . Network
config : dict
Example
- - - - - - -
scenario :
opts : [ Co2L - CCL - 24 H ]
electricity :
agg_p_nom_limits : data / agg_p_nom_minmax . csv
"""
2023-03-09 21:51:56 +00:00
agg_p_nom_minmax = pd . read_csv (
config [ " electricity " ] [ " agg_p_nom_limits " ] , index_col = [ 0 , 1 ]
2022-09-16 13:04:04 +00:00
)
2023-03-09 21:51:56 +00:00
logger . info ( " Adding generation capacity constraints per carrier and country " )
p_nom = n . model [ " Generator-p_nom " ]
gens = n . generators . query ( " p_nom_extendable " ) . rename_axis ( index = " Generator-ext " )
2023-06-29 14:21:45 +00:00
grouper = pd . concat ( [ gens . bus . map ( n . buses . country ) , gens . carrier ] )
2023-03-09 21:51:56 +00:00
lhs = p_nom . groupby ( grouper ) . sum ( ) . rename ( bus = " country " )
minimum = xr . DataArray ( agg_p_nom_minmax [ " min " ] . dropna ( ) ) . rename ( dim_0 = " group " )
index = minimum . indexes [ " group " ] . intersection ( lhs . indexes [ " group " ] )
if not index . empty :
n . model . add_constraints (
lhs . sel ( group = index ) > = minimum . loc [ index ] , name = " agg_p_nom_min "
2022-09-16 13:04:04 +00:00
)
2023-03-09 13:24:25 +00:00
2023-03-09 21:51:56 +00:00
maximum = xr . DataArray ( agg_p_nom_minmax [ " max " ] . dropna ( ) ) . rename ( dim_0 = " group " )
index = maximum . indexes [ " group " ] . intersection ( lhs . indexes [ " group " ] )
if not index . empty :
n . model . add_constraints (
lhs . sel ( group = index ) < = maximum . loc [ index ] , name = " agg_p_nom_max "
)
2020-02-10 11:06:43 +00:00
2020-09-26 11:10:50 +00:00
def add_EQ_constraints ( n , o , scaling = 1e-1 ) :
2023-03-09 13:24:25 +00:00
"""
2023-03-09 21:51:56 +00:00
Add equity constraints to the network .
Currently this is only implemented for the electricity sector only .
2023-03-09 13:24:25 +00:00
Opts must be specified in the config . yaml .
Parameters
- - - - - - - - - -
n : pypsa . Network
o : str
Example
- - - - - - -
scenario :
opts : [ Co2L - EQ0 .7 - 24 H ]
Require each country or node to on average produce a minimal share
2023-03-09 21:51:56 +00:00
of its total electricity consumption itself . Example : EQ0 .7 c demands each country
2023-03-09 13:24:25 +00:00
to produce on average at least 70 % of its consumption ; EQ0 .7 demands
each node to produce on average at least 70 % of its consumption .
"""
2023-03-09 21:51:56 +00:00
# TODO: Generalize to cover myopic and other sectors?
2020-09-26 11:10:50 +00:00
float_regex = " [0-9]* \ .?[0-9]+ "
level = float ( re . findall ( float_regex , o ) [ 0 ] )
2022-09-16 13:04:04 +00:00
if o [ - 1 ] == " c " :
2023-06-12 18:40:51 +00:00
ggrouper = n . generators . bus . map ( n . buses . country )
lgrouper = n . loads . bus . map ( n . buses . country )
sgrouper = n . storage_units . bus . map ( n . buses . country )
2020-09-26 11:10:50 +00:00
else :
2023-06-12 18:40:51 +00:00
ggrouper = n . generators . bus
lgrouper = n . loads . bus
sgrouper = n . storage_units . bus
2022-09-16 13:04:04 +00:00
load = (
n . snapshot_weightings . generators
@ n . loads_t . p_set . groupby ( lgrouper , axis = 1 ) . sum ( )
)
inflow = (
n . snapshot_weightings . stores
@ n . storage_units_t . inflow . groupby ( sgrouper , axis = 1 ) . sum ( )
)
inflow = inflow . reindex ( load . index ) . fillna ( 0.0 )
rhs = scaling * ( level * load - inflow )
2023-03-09 21:51:56 +00:00
p = n . model [ " Generator-p " ]
2022-09-16 13:04:04 +00:00
lhs_gen = (
2023-03-09 21:51:56 +00:00
( p * ( n . snapshot_weightings . generators * scaling ) )
2023-06-12 18:40:51 +00:00
. groupby ( ggrouper . to_xarray ( ) )
2023-03-09 21:51:56 +00:00
. sum ( )
2023-03-09 13:24:25 +00:00
. sum ( " snapshot " )
2022-09-16 13:04:04 +00:00
)
2023-03-09 21:51:56 +00:00
# TODO: double check that this is really needed, why do have to subtract the spillage
2022-12-16 11:00:39 +00:00
if not n . storage_units_t . inflow . empty :
2023-03-09 21:51:56 +00:00
spillage = n . model [ " StorageUnit-spill " ]
2022-12-16 11:00:39 +00:00
lhs_spill = (
2023-03-09 21:51:56 +00:00
( spillage * ( - n . snapshot_weightings . stores * scaling ) )
2023-06-12 18:40:51 +00:00
. groupby ( sgrouper . to_xarray ( ) )
2023-03-09 21:51:56 +00:00
. sum ( )
2023-03-09 13:24:25 +00:00
. sum ( " snapshot " )
2022-09-16 13:04:04 +00:00
)
2023-03-09 21:51:56 +00:00
lhs = lhs_gen + lhs_spill
2022-12-16 11:00:39 +00:00
else :
lhs = lhs_gen
2023-03-09 13:24:25 +00:00
n . model . add_constraints ( lhs > = rhs , name = " equity_min " )
2020-09-26 11:10:50 +00:00
2020-02-10 11:06:43 +00:00
def add_BAU_constraints ( n , config ) :
2023-03-09 13:24:25 +00:00
"""
Add a per - carrier minimal overall capacity .
BAU_mincapacities and opts must be adjusted in the config . yaml .
Parameters
- - - - - - - - - -
n : pypsa . Network
config : dict
Example
- - - - - - -
scenario :
opts : [ Co2L - BAU - 24 H ]
electricity :
BAU_mincapacities :
solar : 0
onwind : 0
OCGT : 100000
offwind - ac : 0
offwind - dc : 0
Which sets minimum expansion across all nodes e . g . in Europe to 100 GW .
OCGT bus 1 + OCGT bus 2 + . . . > 100000
"""
2022-09-16 13:04:04 +00:00
mincaps = pd . Series ( config [ " electricity " ] [ " BAU_mincapacities " ] )
2023-03-09 21:51:56 +00:00
p_nom = n . model [ " Generator-p_nom " ]
2023-03-09 13:24:25 +00:00
ext_i = n . generators . query ( " p_nom_extendable " )
2023-03-09 21:51:56 +00:00
ext_carrier_i = xr . DataArray ( ext_i . carrier . rename_axis ( " Generator-ext " ) )
lhs = p_nom . groupby ( ext_carrier_i ) . sum ( )
index = mincaps . index . intersection ( lhs . indexes [ " carrier " ] )
rhs = mincaps [ index ] . rename_axis ( " carrier " )
2023-03-09 13:24:25 +00:00
n . model . add_constraints ( lhs > = rhs , name = " bau_mincaps " )
2020-02-10 11:06:43 +00:00
2023-03-09 21:51:56 +00:00
# TODO: think about removing or make per country
2020-02-10 11:06:43 +00:00
def add_SAFE_constraints ( n , config ) :
2023-03-09 13:24:25 +00:00
"""
Add a capacity reserve margin of a certain fraction above the peak demand .
Renewable generators and storage do not contribute . Ignores network .
Parameters
- - - - - - - - - -
n : pypsa . Network
config : dict
Example
- - - - - - -
config . yaml requires to specify opts :
scenario :
opts : [ Co2L - SAFE - 24 H ]
electricity :
SAFE_reservemargin : 0.1
Which sets a reserve margin of 10 % above the peak demand .
"""
peakdemand = n . loads_t . p_set . sum ( axis = 1 ) . max ( )
margin = 1.0 + config [ " electricity " ] [ " SAFE_reservemargin " ]
reserve_margin = peakdemand * margin
2023-07-05 09:34:07 +00:00
conventional_carriers = config [ " electricity " ] [ " conventional_carriers " ]
ext_gens_i = n . generators . query (
" carrier in @conventional_carriers & p_nom_extendable "
) . index
2023-03-09 21:51:56 +00:00
p_nom = n . model [ " Generator-p_nom " ] . loc [ ext_gens_i ]
lhs = p_nom . sum ( )
2022-09-16 13:04:04 +00:00
exist_conv_caps = n . generators . query (
2023-07-05 09:34:07 +00:00
" ~p_nom_extendable & carrier in @conventional_carriers "
2022-09-16 13:04:04 +00:00
) . p_nom . sum ( )
2023-03-09 13:24:25 +00:00
rhs = reserve_margin - exist_conv_caps
n . model . add_constraints ( lhs > = rhs , name = " safe_mintotalcap " )
2020-02-10 11:06:43 +00:00
2023-03-09 21:51:56 +00:00
def add_operational_reserve_margin ( n , sns , config ) :
2023-03-09 13:24:25 +00:00
"""
2023-03-09 21:51:56 +00:00
Build reserve margin constraints based on the formulation given in
https : / / genxproject . github . io / GenX / dev / core / #Reserves.
2023-03-09 13:24:25 +00:00
Parameters
- - - - - - - - - -
n : pypsa . Network
2023-03-09 21:51:56 +00:00
sns : pd . DatetimeIndex
2023-03-09 13:24:25 +00:00
config : dict
Example :
- - - - - - - -
config . yaml requires to specify operational_reserve :
operational_reserve : # like https://genxproject.github.io/GenX/dev/core/#Reserves
activate : true
epsilon_load : 0.02 # percentage of load at each snapshot
epsilon_vres : 0.02 # percentage of VRES at each snapshot
contingency : 400000 # MW
"""
2022-04-07 12:39:34 +00:00
reserve_config = config [ " electricity " ] [ " operational_reserve " ]
EPSILON_LOAD = reserve_config [ " epsilon_load " ]
EPSILON_VRES = reserve_config [ " epsilon_vres " ]
CONTINGENCY = reserve_config [ " contingency " ]
2022-09-16 13:04:04 +00:00
# Reserve Variables
2023-03-09 13:24:25 +00:00
n . model . add_variables (
0 , np . inf , coords = [ sns , n . generators . index ] , name = " Generator-r "
)
reserve = n . model [ " Generator-r " ]
2023-04-20 07:35:53 +00:00
summed_reserve = reserve . sum ( " Generator " )
2022-04-07 12:39:34 +00:00
# Share of extendable renewable capacities
2022-09-16 13:04:04 +00:00
ext_i = n . generators . query ( " p_nom_extendable " ) . index
2022-04-07 12:39:34 +00:00
vres_i = n . generators_t . p_max_pu . columns
2022-04-07 15:20:56 +00:00
if not ext_i . empty and not vres_i . empty :
capacity_factor = n . generators_t . p_max_pu [ vres_i . intersection ( ext_i ) ]
2023-03-09 21:51:56 +00:00
p_nom_vres = (
2023-03-09 13:24:25 +00:00
n . model [ " Generator-p_nom " ]
2023-03-09 21:51:56 +00:00
. loc [ vres_i . intersection ( ext_i ) ]
2023-03-09 13:24:25 +00:00
. rename ( { " Generator-ext " : " Generator " } )
)
2023-04-20 07:39:15 +00:00
lhs = summed_reserve + ( p_nom_vres * ( - EPSILON_VRES * capacity_factor ) ) . sum (
" Generator "
)
2023-03-09 13:24:25 +00:00
# Total demand per t
2023-04-20 07:35:53 +00:00
demand = get_as_dense ( n , " Load " , " p_set " ) . sum ( axis = 1 )
2022-09-16 13:04:04 +00:00
2022-04-07 12:39:34 +00:00
# VRES potential of non extendable generators
capacity_factor = n . generators_t . p_max_pu [ vres_i . difference ( ext_i ) ]
renewable_capacity = n . generators . p_nom [ vres_i . difference ( ext_i ) ]
2023-03-09 21:51:56 +00:00
potential = ( capacity_factor * renewable_capacity ) . sum ( axis = 1 )
2022-09-16 13:04:04 +00:00
2022-04-07 12:39:34 +00:00
# Right-hand-side
rhs = EPSILON_LOAD * demand + EPSILON_VRES * potential + CONTINGENCY
2022-09-16 13:04:04 +00:00
2023-03-09 13:24:25 +00:00
n . model . add_constraints ( lhs > = rhs , name = " reserve_margin " )
2022-04-07 12:39:34 +00:00
2023-04-21 08:11:44 +00:00
# additional constraint that capacity is not exceeded
2023-04-20 07:35:53 +00:00
gen_i = n . generators . index
2023-04-20 07:39:15 +00:00
ext_i = n . generators . query ( " p_nom_extendable " ) . index
fix_i = n . generators . query ( " not p_nom_extendable " ) . index
2023-04-20 07:35:53 +00:00
dispatch = n . model [ " Generator-p " ]
2023-03-09 21:51:56 +00:00
reserve = n . model [ " Generator-r " ]
2022-04-07 12:39:34 +00:00
2023-04-20 07:39:15 +00:00
capacity_variable = n . model [ " Generator-p_nom " ] . rename (
{ " Generator-ext " : " Generator " }
)
2023-04-20 07:35:53 +00:00
capacity_fixed = n . generators . p_nom [ fix_i ]
2023-04-20 07:39:15 +00:00
p_max_pu = get_as_dense ( n , " Generator " , " p_max_pu " )
2023-04-20 07:35:53 +00:00
lhs = dispatch + reserve - capacity_variable * p_max_pu [ ext_i ]
rhs = ( p_max_pu [ fix_i ] * capacity_fixed ) . reindex ( columns = gen_i , fill_value = 0 )
2023-03-09 13:24:25 +00:00
2023-04-20 07:55:50 +00:00
n . model . add_constraints ( lhs < = rhs , name = " Generator-p-reserve-upper " )
2023-03-09 21:51:56 +00:00
def add_battery_constraints ( n ) :
2023-03-09 13:24:25 +00:00
"""
2023-03-09 21:51:56 +00:00
Add constraint ensuring that charger = discharger , i . e .
1 * charger_size - efficiency * discharger_size = 0
"""
if not n . links . p_nom_extendable . any ( ) :
return
2022-09-16 13:04:04 +00:00
2023-03-09 21:51:56 +00:00
discharger_bool = n . links . index . str . contains ( " battery discharger " )
charger_bool = n . links . index . str . contains ( " battery charger " )
dischargers_ext = n . links [ discharger_bool ] . query ( " p_nom_extendable " ) . index
chargers_ext = n . links [ charger_bool ] . query ( " p_nom_extendable " ) . index
2022-09-16 13:04:04 +00:00
2023-03-09 21:51:56 +00:00
eff = n . links . efficiency [ dischargers_ext ] . values
lhs = (
n . model [ " Link-p_nom " ] . loc [ chargers_ext ]
- n . model [ " Link-p_nom " ] . loc [ dischargers_ext ] * eff
2023-03-09 13:24:25 +00:00
)
2022-09-16 13:04:04 +00:00
2023-03-09 21:51:56 +00:00
n . model . add_constraints ( lhs == 0 , name = " Link-charger_ratio " )
2022-09-16 13:04:04 +00:00
2023-03-09 21:51:56 +00:00
def add_chp_constraints ( n ) :
electric = (
n . links . index . str . contains ( " urban central " )
& n . links . index . str . contains ( " CHP " )
& n . links . index . str . contains ( " electric " )
)
heat = (
n . links . index . str . contains ( " urban central " )
& n . links . index . str . contains ( " CHP " )
& n . links . index . str . contains ( " heat " )
2023-03-09 13:24:25 +00:00
)
2022-04-07 12:39:34 +00:00
2023-03-09 21:51:56 +00:00
electric_ext = n . links [ electric ] . query ( " p_nom_extendable " ) . index
heat_ext = n . links [ heat ] . query ( " p_nom_extendable " ) . index
2022-04-07 12:39:34 +00:00
2023-03-09 21:51:56 +00:00
electric_fix = n . links [ electric ] . query ( " ~p_nom_extendable " ) . index
heat_fix = n . links [ heat ] . query ( " ~p_nom_extendable " ) . index
2022-09-16 13:04:04 +00:00
2023-03-09 21:51:56 +00:00
p = n . model [ " Link-p " ] # dimension: [time, link]
2022-04-07 12:39:34 +00:00
2023-03-09 21:51:56 +00:00
# output ratio between heat and electricity and top_iso_fuel_line for extendable
if not electric_ext . empty :
p_nom = n . model [ " Link-p_nom " ]
2022-04-07 12:39:34 +00:00
2023-03-09 21:51:56 +00:00
lhs = (
p_nom . loc [ electric_ext ]
* ( n . links . p_nom_ratio * n . links . efficiency ) [ electric_ext ] . values
- p_nom . loc [ heat_ext ] * n . links . efficiency [ heat_ext ] . values
)
n . model . add_constraints ( lhs == 0 , name = " chplink-fix_p_nom_ratio " )
2023-03-09 13:24:25 +00:00
2023-03-09 21:51:56 +00:00
rename = { " Link-ext " : " Link " }
lhs = (
p . loc [ : , electric_ext ]
+ p . loc [ : , heat_ext ]
- p_nom . rename ( rename ) . loc [ electric_ext ]
)
n . model . add_constraints ( lhs < = 0 , name = " chplink-top_iso_fuel_line_ext " )
# top_iso_fuel_line for fixed
if not electric_fix . empty :
lhs = p . loc [ : , electric_fix ] + p . loc [ : , heat_fix ]
rhs = n . links . p_nom [ electric_fix ]
n . model . add_constraints ( lhs < = rhs , name = " chplink-top_iso_fuel_line_fix " )
# back-pressure
if not electric . empty :
lhs = (
p . loc [ : , heat ] * ( n . links . efficiency [ heat ] * n . links . c_b [ electric ] . values )
- p . loc [ : , electric ] * n . links . efficiency [ electric ]
)
n . model . add_constraints ( lhs < = rhs , name = " chplink-backpressure " )
def add_pipe_retrofit_constraint ( n ) :
2023-03-09 13:24:25 +00:00
"""
2023-03-09 21:51:56 +00:00
Add constraint for retrofitting existing CH4 pipelines to H2 pipelines .
"""
gas_pipes_i = n . links . query ( " carrier == ' gas pipeline ' and p_nom_extendable " ) . index
h2_retrofitted_i = n . links . query (
" carrier == ' H2 pipeline retrofitted ' and p_nom_extendable "
) . index
if h2_retrofitted_i . empty or gas_pipes_i . empty :
2020-02-10 11:06:43 +00:00
return
2023-03-09 21:51:56 +00:00
p_nom = n . model [ " Link-p_nom " ]
2020-02-10 11:06:43 +00:00
2023-03-09 21:51:56 +00:00
CH4_per_H2 = 1 / n . config [ " sector " ] [ " H2_retrofit_capacity_per_CH4 " ]
lhs = p_nom . loc [ gas_pipes_i ] + CH4_per_H2 * p_nom . loc [ h2_retrofitted_i ]
rhs = n . links . p_nom [ gas_pipes_i ] . rename_axis ( " Link-ext " )
2022-09-16 13:04:04 +00:00
2023-03-09 21:51:56 +00:00
n . model . add_constraints ( lhs == rhs , name = " Link-pipe_retrofit " )
2023-10-24 14:39:58 +00:00
def add_co2limit_country ( n , limit_countries , nyears = 1.0 ) :
2023-10-18 14:59:49 +00:00
"""
Add a set of emissions limit constraints for specified countries .
The countries and emissions limits are specified in the config file entry ' co2_budget_country_ {investment_year} ' .
Parameters
- - - - - - - - - -
n : pypsa . Network
config : dict
limit_countries : dict
nyears : float , optional
Used to scale the emissions constraint to the number of snapshots of the base network .
"""
logger . info ( f " Adding CO2 budget limit for each country as per unit of 1990 levels " )
2023-10-24 14:39:58 +00:00
countries = n . config [ " countries " ]
2023-10-18 14:59:49 +00:00
# TODO: import function from prepare_sector_network? Move to common place?
sectors = emission_sectors_from_opts ( opts )
# convert Mt to tCO2
co2_totals = 1e6 * pd . read_csv ( snakemake . input . co2_totals_name , index_col = 0 )
co2_limit_countries = co2_totals . loc [ countries , sectors ] . sum ( axis = 1 )
co2_limit_countries = co2_limit_countries . loc [ co2_limit_countries . index . isin ( limit_countries . keys ( ) ) ]
co2_limit_countries * = co2_limit_countries . index . map ( limit_countries ) * nyears
p = n . model [ " Link-p " ] # dimension: (time, component)
2023-11-21 13:55:32 +00:00
# NB: Most country-specific links retain their locational information in bus1 (except for DAC, where it is in bus2, and process emissions, where it is in bus0)
2023-10-18 14:59:49 +00:00
country = n . links . bus1 . map ( n . buses . location ) . map ( n . buses . country )
country_DAC = (
n . links [ n . links . carrier == " DAC " ]
. bus2 . map ( n . buses . location )
. map ( n . buses . country )
)
country [ country_DAC . index ] = country_DAC
2023-11-21 13:55:32 +00:00
country_process_emissions = (
n . links [ n . links . carrier . str . contains ( " process emissions " ) ]
. bus0 . map ( n . buses . location )
. map ( n . buses . country )
)
country [ country_process_emissions . index ] = country_process_emissions
2023-10-18 14:59:49 +00:00
lhs = [ ]
for port in [ col [ 3 : ] for col in n . links if col . startswith ( " bus " ) ] :
if port == str ( 0 ) :
efficiency = (
n . links [ " efficiency " ] . apply ( lambda x : 1.0 ) . rename ( " efficiency0 " )
)
elif port == str ( 1 ) :
2023-10-24 14:39:58 +00:00
efficiency = n . links [ " efficiency " ]
2023-10-18 14:59:49 +00:00
else :
efficiency = n . links [ f " efficiency { port } " ]
mask = n . links [ f " bus { port } " ] . map ( n . buses . carrier ) . eq ( " co2 " )
idx = n . links [ mask ] . index
2023-11-21 13:55:32 +00:00
international = n . links . carrier . map (
lambda x : 0.4 if x in [ " kerosene for aviation " , " shipping oil " ] else 1.0
)
2023-10-18 14:59:49 +00:00
grouping = country . loc [ idx ]
if not grouping . isnull ( ) . all ( ) :
expr = (
2023-11-21 13:55:32 +00:00
( ( p . loc [ : , idx ] * efficiency [ idx ] * international [ idx ] )
2023-10-18 14:59:49 +00:00
. groupby ( grouping , axis = 1 )
. sum ( )
2023-11-21 13:55:32 +00:00
* n . snapshot_weightings . generators
)
2023-10-18 14:59:49 +00:00
. sum ( dims = " snapshot " )
)
lhs . append ( expr )
lhs = sum ( lhs ) # dimension: (country)
lhs = lhs . rename ( { list ( lhs . dims . keys ( ) ) [ 0 ] : " country " } )
rhs = pd . Series ( co2_limit_countries ) # dimension: (country)
for ct in lhs . indexes [ " country " ] :
n . model . add_constraints (
lhs . loc [ ct ] < = rhs [ ct ] ,
name = f " GlobalConstraint-co2_limit_per_country { ct } " ,
)
n . add (
" GlobalConstraint " ,
f " co2_limit_per_country { ct } " ,
constant = rhs [ ct ] ,
sense = " <= " ,
type = " " ,
)
2023-03-09 21:51:56 +00:00
def extra_functionality ( n , snapshots ) :
2023-03-10 14:58:53 +00:00
"""
Collects supplementary constraints which will be passed to
` ` pypsa . optimization . optimize ` ` .
If you want to enforce additional custom constraints , this is a good
location to add them . The arguments ` ` opts ` ` and
` ` snakemake . config ` ` are expected to be attached to the network .
"""
opts = n . opts
config = n . config
if " BAU " in opts and n . generators . p_nom_extendable . any ( ) :
add_BAU_constraints ( n , config )
if " SAFE " in opts and n . generators . p_nom_extendable . any ( ) :
add_SAFE_constraints ( n , config )
if " CCL " in opts and n . generators . p_nom_extendable . any ( ) :
add_CCL_constraints ( n , config )
reserve = config [ " electricity " ] . get ( " operational_reserve " , { } )
if reserve . get ( " activate " ) :
add_operational_reserve_margin ( n , snapshots , config )
for o in opts :
if " EQ " in o :
add_EQ_constraints ( n , o )
2020-02-10 11:06:43 +00:00
add_battery_constraints ( n )
2023-03-09 21:51:56 +00:00
add_pipe_retrofit_constraint ( n )
2023-08-25 08:49:03 +00:00
if n . _multi_invest :
2023-08-25 13:50:26 +00:00
add_carbon_constraint ( n , snapshots )
2023-08-28 08:21:02 +00:00
add_carbon_budget_constraint ( n , snapshots )
2023-08-28 11:31:02 +00:00
add_retrofit_gas_boiler_constraint ( n , snapshots )
2020-02-10 11:06:43 +00:00
2023-10-18 14:59:49 +00:00
if n . config [ " sector " ] [ " co2_budget_national " ] :
# prepare co2 constraint
nhours = n . snapshot_weightings . generators . sum ( )
nyears = nhours / 8760
investment_year = int ( snakemake . wildcards . planning_horizons [ - 4 : ] )
limit_countries = snakemake . config [ " co2_budget_national " ] [ investment_year ]
# add co2 constraint for each country
logger . info ( f " Add CO2 limit for each country " )
2023-10-24 14:39:58 +00:00
add_co2limit_country ( n , limit_countries , nyears )
2023-10-18 14:59:49 +00:00
2020-02-10 11:06:43 +00:00
2023-06-15 16:52:25 +00:00
def solve_network ( n , config , solving , opts = " " , * * kwargs ) :
set_of_options = solving [ " solver " ] [ " options " ]
cf_solving = solving [ " options " ]
2023-08-30 10:05:16 +00:00
2023-10-08 09:20:36 +00:00
kwargs [ " multi_investment_periods " ] = config [ " foresight " ] == " perfect "
2023-07-11 15:48:35 +00:00
kwargs [ " solver_options " ] = (
solving [ " solver_options " ] [ set_of_options ] if set_of_options else { }
)
kwargs [ " solver_name " ] = solving [ " solver " ] [ " name " ]
kwargs [ " extra_functionality " ] = extra_functionality
kwargs [ " transmission_losses " ] = cf_solving . get ( " transmission_losses " , False )
kwargs [ " linearized_unit_commitment " ] = cf_solving . get (
" linearized_unit_commitment " , False
)
2023-07-31 09:20:11 +00:00
kwargs [ " assign_all_duals " ] = cf_solving . get ( " assign_all_duals " , False )
2020-02-10 15:47:11 +00:00
2023-07-11 15:48:35 +00:00
rolling_horizon = cf_solving . pop ( " rolling_horizon " , False )
skip_iterations = cf_solving . pop ( " skip_iterations " , False )
2022-03-24 13:30:28 +00:00
if not n . lines . s_nom_extendable . any ( ) :
skip_iterations = True
logger . info ( " No expandable lines found. Skipping iterative solving. " )
2023-07-11 15:48:35 +00:00
# add to network for extra_functionality
n . config = config
n . opts = opts
if rolling_horizon :
kwargs [ " horizon " ] = cf_solving . get ( " horizon " , 365 )
kwargs [ " overlap " ] = cf_solving . get ( " overlap " , 0 )
n . optimize . optimize_with_rolling_horizon ( * * kwargs )
status , condition = " " , " "
elif skip_iterations :
status , condition = n . optimize ( * * kwargs )
2019-06-18 11:24:29 +00:00
else :
2023-07-11 15:48:35 +00:00
kwargs [ " track_iterations " ] = ( cf_solving . get ( " track_iterations " , False ) , )
kwargs [ " min_iterations " ] = ( cf_solving . get ( " min_iterations " , 4 ) , )
kwargs [ " max_iterations " ] = ( cf_solving . get ( " max_iterations " , 6 ) , )
2023-03-09 21:51:56 +00:00
status , condition = n . optimize . optimize_transmission_expansion_iteratively (
2023-07-11 15:48:35 +00:00
* * kwargs
2022-09-16 13:04:04 +00:00
)
2023-03-09 13:24:25 +00:00
2023-07-11 15:48:35 +00:00
if status != " ok " and not rolling_horizon :
2023-03-09 21:51:56 +00:00
logger . warning (
f " Solving status ' { status } ' with termination condition ' { condition } ' "
)
if " infeasible " in condition :
2023-11-21 13:55:32 +00:00
m = n . model
labels = m . compute_infeasibilities ( )
print ( labels )
m . print_infeasibilities ( )
2023-03-09 21:51:56 +00:00
raise RuntimeError ( " Solving status ' infeasible ' " )
2017-12-18 19:31:27 +00:00
return n
2023-08-30 10:05:16 +00:00
2017-12-18 19:31:27 +00:00
if __name__ == " __main__ " :
2022-09-16 13:04:04 +00:00
if " snakemake " not in globals ( ) :
2019-12-09 20:29:15 +00:00
from _helpers import mock_snakemake
2022-09-16 13:04:04 +00:00
snakemake = mock_snakemake (
2023-08-24 06:42:24 +00:00
" solve_sector_network_perfect " ,
2023-08-30 13:57:47 +00:00
configfiles = " ../config/test/config.perfect.yaml " ,
2023-03-09 13:24:25 +00:00
simpl = " " ,
2023-03-09 21:51:56 +00:00
opts = " " ,
2023-08-30 13:57:47 +00:00
clusters = " 5 " ,
ll = " v1.5 " ,
sector_opts = " 8760H-T-H-B-I-A-solar+p3-dist1 " ,
2023-03-09 21:51:56 +00:00
planning_horizons = " 2030 " ,
2022-09-16 13:04:04 +00:00
)
2019-11-28 07:22:52 +00:00
configure_logging ( snakemake )
2023-03-10 16:14:01 +00:00
if " sector_opts " in snakemake . wildcards . keys ( ) :
update_config_with_sector_opts (
snakemake . config , snakemake . wildcards . sector_opts
)
2019-11-28 07:22:52 +00:00
2023-03-10 16:14:01 +00:00
opts = snakemake . wildcards . opts
if " sector_opts " in snakemake . wildcards . keys ( ) :
2023-03-10 16:30:51 +00:00
opts + = " - " + snakemake . wildcards . sector_opts
2023-03-10 16:14:01 +00:00
opts = [ o for o in opts . split ( " - " ) if o != " " ]
2023-06-15 16:52:25 +00:00
solve_opts = snakemake . params . solving [ " options " ]
2018-02-01 11:42:56 +00:00
2023-03-09 21:51:56 +00:00
np . random . seed ( solve_opts . get ( " seed " , 123 ) )
2023-07-13 20:31:55 +00:00
n = pypsa . Network ( snakemake . input . network )
2023-08-30 10:05:16 +00:00
2023-05-27 10:23:28 +00:00
n = prepare_network (
2023-06-02 10:52:49 +00:00
n ,
solve_opts ,
config = snakemake . config ,
2023-06-15 16:52:25 +00:00
foresight = snakemake . params . foresight ,
planning_horizons = snakemake . params . planning_horizons ,
co2_sequestration_potential = snakemake . params [ " co2_sequestration_potential " ] ,
2023-05-27 10:23:28 +00:00
)
2023-08-30 10:05:16 +00:00
with memory_logger (
filename = getattr ( snakemake . log , " memory " , None ) , interval = 30.0
) as mem :
2023-08-24 13:48:06 +00:00
n = solve_network (
n ,
config = snakemake . config ,
solving = snakemake . params . solving ,
opts = opts ,
log_fn = snakemake . log . solver ,
)
2023-03-09 21:51:56 +00:00
2023-10-08 09:20:36 +00:00
logger . info ( f " Maximum memory usage: { mem . mem_usage } " )
2017-12-18 19:31:27 +00:00
2023-05-11 14:58:35 +00:00
n . meta = dict ( snakemake . config , * * dict ( wildcards = dict ( snakemake . wildcards ) ) )
n . export_to_netcdf ( snakemake . output [ 0 ] )