Merge branch 'master' into wildcard-opts-config

This commit is contained in:
Fabian Neumann 2024-01-05 12:02:11 +01:00 committed by GitHub
commit f68e8d3d61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 791 additions and 371 deletions

View File

@ -14,7 +14,7 @@ from snakemake.utils import min_version
min_version("7.7") min_version("7.7")
if not exists("config/config.yaml"): if not exists("config/config.yaml") and exists("config/config.default.yaml"):
copyfile("config/config.default.yaml", "config/config.yaml") copyfile("config/config.default.yaml", "config/config.yaml")
@ -125,6 +125,7 @@ rule sync:
shell: shell:
""" """
rsync -uvarh --ignore-missing-args --files-from=.sync-send . {params.cluster} rsync -uvarh --ignore-missing-args --files-from=.sync-send . {params.cluster}
rsync -uvarh --no-g {params.cluster}/resources . || echo "No resources directory, skipping rsync"
rsync -uvarh --no-g {params.cluster}/results . || echo "No results directory, skipping rsync" rsync -uvarh --no-g {params.cluster}/results . || echo "No results directory, skipping rsync"
rsync -uvarh --no-g {params.cluster}/logs . || echo "No logs directory, skipping rsync" rsync -uvarh --no-g {params.cluster}/logs . || echo "No logs directory, skipping rsync"
""" """

View File

@ -176,9 +176,12 @@ renewable:
grid_codes: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 32] grid_codes: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 32]
distance: 1000 distance: 1000
distance_grid_codes: [1, 2, 3, 4, 5, 6] distance_grid_codes: [1, 2, 3, 4, 5, 6]
luisa: false
# grid_codes: [1111, 1121, 1122, 1123, 1130, 1210, 1221, 1222, 1230, 1241, 1242]
# distance: 1000
# distance_grid_codes: [1111, 1121, 1122, 1123, 1130, 1210, 1221, 1222, 1230, 1241, 1242]
natura: true natura: true
excluder_resolution: 100 excluder_resolution: 100
potential: simple # or conservative
clip_p_max_pu: 1.e-2 clip_p_max_pu: 1.e-2
offwind-ac: offwind-ac:
cutout: europe-2013-era5 cutout: europe-2013-era5
@ -189,12 +192,12 @@ renewable:
capacity_per_sqkm: 2 capacity_per_sqkm: 2
correction_factor: 0.8855 correction_factor: 0.8855
corine: [44, 255] corine: [44, 255]
luisa: false # [0, 5230]
natura: true natura: true
ship_threshold: 400 ship_threshold: 400
max_depth: 50 max_depth: 50
max_shore_distance: 30000 max_shore_distance: 30000
excluder_resolution: 200 excluder_resolution: 200
potential: simple # or conservative
clip_p_max_pu: 1.e-2 clip_p_max_pu: 1.e-2
offwind-dc: offwind-dc:
cutout: europe-2013-era5 cutout: europe-2013-era5
@ -205,12 +208,12 @@ renewable:
capacity_per_sqkm: 2 capacity_per_sqkm: 2
correction_factor: 0.8855 correction_factor: 0.8855
corine: [44, 255] corine: [44, 255]
luisa: false # [0, 5230]
natura: true natura: true
ship_threshold: 400 ship_threshold: 400
max_depth: 50 max_depth: 50
min_shore_distance: 30000 min_shore_distance: 30000
excluder_resolution: 200 excluder_resolution: 200
potential: simple # or conservative
clip_p_max_pu: 1.e-2 clip_p_max_pu: 1.e-2
solar: solar:
cutout: europe-2013-sarah cutout: europe-2013-sarah
@ -220,12 +223,12 @@ renewable:
orientation: orientation:
slope: 35. slope: 35.
azimuth: 180. azimuth: 180.
capacity_per_sqkm: 1.7 capacity_per_sqkm: 5.1
# correction_factor: 0.854337 # correction_factor: 0.854337
corine: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 26, 31, 32] corine: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 26, 31, 32]
luisa: false # [1111, 1121, 1122, 1123, 1130, 1210, 1221, 1222, 1230, 1241, 1242, 1310, 1320, 1330, 1410, 1421, 1422, 2110, 2120, 2130, 2210, 2220, 2230, 2310, 2410, 2420, 3210, 3320, 3330]
natura: true natura: true
excluder_resolution: 100 excluder_resolution: 100
potential: simple # or conservative
clip_p_max_pu: 1.e-2 clip_p_max_pu: 1.e-2
hydro: hydro:
cutout: europe-2013-era5 cutout: europe-2013-era5
@ -303,6 +306,7 @@ pypsa_eur:
- offwind-dc - offwind-dc
- solar - solar
- ror - ror
- nuclear
StorageUnit: StorageUnit:
- PHS - PHS
- hydro - hydro
@ -389,14 +393,14 @@ sector:
v2g: true v2g: true
land_transport_fuel_cell_share: land_transport_fuel_cell_share:
2020: 0 2020: 0
2030: 0.05 2030: 0
2040: 0.1 2040: 0
2050: 0.15 2050: 0
land_transport_electric_share: land_transport_electric_share:
2020: 0 2020: 0
2030: 0.25 2030: 0.3
2040: 0.6 2040: 0.7
2050: 0.85 2050: 1
land_transport_ice_share: land_transport_ice_share:
2020: 1 2020: 1
2030: 0.7 2030: 0.7
@ -462,7 +466,6 @@ sector:
solar_cf_correction: 0.788457 # = >>> 1/1.2683 solar_cf_correction: 0.788457 # = >>> 1/1.2683
marginal_cost_storage: 0. #1e-4 marginal_cost_storage: 0. #1e-4
methanation: true methanation: true
helmeth: false
coal_cc: false coal_cc: false
dac: true dac: true
co2_vent: false co2_vent: false
@ -472,6 +475,8 @@ sector:
hydrogen_turbine: false hydrogen_turbine: false
SMR: true SMR: true
SMR_cc: true SMR_cc: true
regional_methanol_demand: false
regional_oil_demand: false
regional_co2_sequestration_potential: regional_co2_sequestration_potential:
enable: false enable: false
attribute: 'conservative estimate Mt' attribute: 'conservative estimate Mt'
@ -484,6 +489,7 @@ sector:
co2_sequestration_lifetime: 50 co2_sequestration_lifetime: 50
co2_spatial: false co2_spatial: false
co2network: false co2network: false
co2_network_cost_factor: 1
cc_fraction: 0.9 cc_fraction: 0.9
hydrogen_underground_storage: true hydrogen_underground_storage: true
hydrogen_underground_storage_locations: hydrogen_underground_storage_locations:
@ -503,6 +509,16 @@ sector:
electricity_distribution_grid: true electricity_distribution_grid: true
electricity_distribution_grid_cost_factor: 1.0 electricity_distribution_grid_cost_factor: 1.0
electricity_grid_connection: true electricity_grid_connection: true
transmission_efficiency:
DC:
efficiency_static: 0.98
efficiency_per_1000km: 0.977
H2 pipeline:
efficiency_per_1000km: 1 # 0.979
compression_per_1000km: 0.019
gas pipeline:
efficiency_per_1000km: 1 #0.977
compression_per_1000km: 0.01
H2_network: true H2_network: true
gas_network: false gas_network: false
H2_retrofit: false H2_retrofit: false
@ -648,11 +664,12 @@ solving:
skip_iterations: true skip_iterations: true
rolling_horizon: false rolling_horizon: false
seed: 123 seed: 123
custom_extra_functionality: "../data/custom_extra_functionality.py"
# options that go into the optimize function # options that go into the optimize function
track_iterations: false track_iterations: false
min_iterations: 4 min_iterations: 4
max_iterations: 6 max_iterations: 6
transmission_losses: 0 transmission_losses: 2
linearized_unit_commitment: true linearized_unit_commitment: true
horizon: 365 horizon: 365
@ -974,7 +991,6 @@ plotting:
Sabatier: '#9850ad' Sabatier: '#9850ad'
methanation: '#c44ce6' methanation: '#c44ce6'
methane: '#c44ce6' methane: '#c44ce6'
helmeth: '#e899ff'
# synfuels # synfuels
Fischer-Tropsch: '#25c49a' Fischer-Tropsch: '#25c49a'
liquid: '#25c49a' liquid: '#25c49a'
@ -989,6 +1005,7 @@ plotting:
CO2 sequestration: '#f29dae' CO2 sequestration: '#f29dae'
DAC: '#ff5270' DAC: '#ff5270'
co2 stored: '#f2385a' co2 stored: '#f2385a'
co2 sequestered: '#f2682f'
co2: '#f29dae' co2: '#f29dae'
co2 vent: '#ffd4dc' co2 vent: '#ffd4dc'
CO2 pipeline: '#f5627f' CO2 pipeline: '#f5627f'

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: : 2023- The PyPSA-Eur Authors
#
# SPDX-License-Identifier: MIT
def custom_extra_functionality(n, snapshots, snakemake):
"""
Add custom extra functionality constraints.
"""
pass

View File

@ -7,10 +7,10 @@ capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of wind turbine place
correction_factor,--,float,"Correction factor for capacity factor time series." correction_factor,--,float,"Correction factor for capacity factor time series."
excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis." excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis."
corine,--,"Any *realistic* subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for AC-connected offshore wind turbine placement." corine,--,"Any *realistic* subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for AC-connected offshore wind turbine placement."
luisa,--,"Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_","Specifies areas according to the LUISA Base Map codes which are generally eligible for AC-connected offshore wind turbine placement."
natura,bool,"{true, false}","Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``." natura,bool,"{true, false}","Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``."
ship_threshold,--,float,"Ship density threshold from which areas are excluded." ship_threshold,--,float,"Ship density threshold from which areas are excluded."
max_depth,m,float,"Maximum sea water depth at which wind turbines can be build. Maritime areas with deeper waters are excluded in the process of calculating the AC-connected offshore wind potential." max_depth,m,float,"Maximum sea water depth at which wind turbines can be build. Maritime areas with deeper waters are excluded in the process of calculating the AC-connected offshore wind potential."
min_shore_distance,m,float,"Minimum distance to the shore below which wind turbines cannot be build. Such areas close to the shore are excluded in the process of calculating the AC-connected offshore wind potential." min_shore_distance,m,float,"Minimum distance to the shore below which wind turbines cannot be build. Such areas close to the shore are excluded in the process of calculating the AC-connected offshore wind potential."
max_shore_distance,m,float,"Maximum distance to the shore above which wind turbines cannot be build. Such areas close to the shore are excluded in the process of calculating the AC-connected offshore wind potential." max_shore_distance,m,float,"Maximum distance to the shore above which wind turbines cannot be build. Such areas close to the shore are excluded in the process of calculating the AC-connected offshore wind potential."
potential,--,"One of {'simple', 'conservative'}","Method to compute the maximal installable potential for a node; confer :ref:`renewableprofiles`"
clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero." clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero."

1 Unit Values Description
7 correction_factor -- float Correction factor for capacity factor time series.
8 excluder_resolution m float Resolution on which to perform geographical elibility analysis.
9 corine -- Any *realistic* subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_ Specifies areas according to CORINE Land Cover codes which are generally eligible for AC-connected offshore wind turbine placement.
10 luisa -- Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_ Specifies areas according to the LUISA Base Map codes which are generally eligible for AC-connected offshore wind turbine placement.
11 natura bool {true, false} Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``.
12 ship_threshold -- float Ship density threshold from which areas are excluded.
13 max_depth m float Maximum sea water depth at which wind turbines can be build. Maritime areas with deeper waters are excluded in the process of calculating the AC-connected offshore wind potential.
14 min_shore_distance m float Minimum distance to the shore below which wind turbines cannot be build. Such areas close to the shore are excluded in the process of calculating the AC-connected offshore wind potential.
15 max_shore_distance m float Maximum distance to the shore above which wind turbines cannot be build. Such areas close to the shore are excluded in the process of calculating the AC-connected offshore wind potential.
potential -- One of {'simple', 'conservative'} Method to compute the maximal installable potential for a node; confer :ref:`renewableprofiles`
16 clip_p_max_pu p.u. float To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero.

View File

@ -7,10 +7,10 @@ capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of wind turbine place
correction_factor,--,float,"Correction factor for capacity factor time series." correction_factor,--,float,"Correction factor for capacity factor time series."
excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis." excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis."
corine,--,"Any *realistic* subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for AC-connected offshore wind turbine placement." corine,--,"Any *realistic* subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for AC-connected offshore wind turbine placement."
luisa,--,"Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_","Specifies areas according to the LUISA Base Map codes which are generally eligible for DC-connected offshore wind turbine placement."
natura,bool,"{true, false}","Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``." natura,bool,"{true, false}","Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``."
ship_threshold,--,float,"Ship density threshold from which areas are excluded." ship_threshold,--,float,"Ship density threshold from which areas are excluded."
max_depth,m,float,"Maximum sea water depth at which wind turbines can be build. Maritime areas with deeper waters are excluded in the process of calculating the AC-connected offshore wind potential." max_depth,m,float,"Maximum sea water depth at which wind turbines can be build. Maritime areas with deeper waters are excluded in the process of calculating the AC-connected offshore wind potential."
min_shore_distance,m,float,"Minimum distance to the shore below which wind turbines cannot be build." min_shore_distance,m,float,"Minimum distance to the shore below which wind turbines cannot be build."
max_shore_distance,m,float,"Maximum distance to the shore above which wind turbines cannot be build." max_shore_distance,m,float,"Maximum distance to the shore above which wind turbines cannot be build."
potential,--,"One of {'simple', 'conservative'}","Method to compute the maximal installable potential for a node; confer :ref:`renewableprofiles`"
clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero." clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero."

1 Unit Values Description
7 correction_factor -- float Correction factor for capacity factor time series.
8 excluder_resolution m float Resolution on which to perform geographical elibility analysis.
9 corine -- Any *realistic* subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_ Specifies areas according to CORINE Land Cover codes which are generally eligible for AC-connected offshore wind turbine placement.
10 luisa -- Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_ Specifies areas according to the LUISA Base Map codes which are generally eligible for DC-connected offshore wind turbine placement.
11 natura bool {true, false} Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``.
12 ship_threshold -- float Ship density threshold from which areas are excluded.
13 max_depth m float Maximum sea water depth at which wind turbines can be build. Maritime areas with deeper waters are excluded in the process of calculating the AC-connected offshore wind potential.
14 min_shore_distance m float Minimum distance to the shore below which wind turbines cannot be build.
15 max_shore_distance m float Maximum distance to the shore above which wind turbines cannot be build.
potential -- One of {'simple', 'conservative'} Method to compute the maximal installable potential for a node; confer :ref:`renewableprofiles`
16 clip_p_max_pu p.u. float To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero.

View File

@ -8,8 +8,11 @@ corine,,,
-- grid_codes,--,"Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for wind turbine placement." -- grid_codes,--,"Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for wind turbine placement."
-- distance,m,float,"Distance to keep from areas specified in ``distance_grid_codes``" -- distance,m,float,"Distance to keep from areas specified in ``distance_grid_codes``"
-- distance_grid_codes,--,"Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes to which wind turbines must maintain a distance specified in the setting ``distance``." -- distance_grid_codes,--,"Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes to which wind turbines must maintain a distance specified in the setting ``distance``."
luisa,,,
-- grid_codes,--,"Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_","Specifies areas according to the LUISA Base Map codes which are generally eligible for wind turbine placement."
-- distance,m,float,"Distance to keep from areas specified in ``distance_grid_codes``"
-- distance_grid_codes,--,"Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_","Specifies areas according to the LUISA Base Map codes to which wind turbines must maintain a distance specified in the setting ``distance``."
natura,bool,"{true, false}","Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``." natura,bool,"{true, false}","Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``."
potential,--,"One of {'simple', 'conservative'}","Method to compute the maximal installable potential for a node; confer :ref:`renewableprofiles`"
clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero." clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero."
correction_factor,--,float,"Correction factor for capacity factor time series." correction_factor,--,float,"Correction factor for capacity factor time series."
excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis." excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis."

1 Unit Values Description
8 -- grid_codes -- Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_ Specifies areas according to CORINE Land Cover codes which are generally eligible for wind turbine placement.
9 -- distance m float Distance to keep from areas specified in ``distance_grid_codes``
10 -- distance_grid_codes -- Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_ Specifies areas according to CORINE Land Cover codes to which wind turbines must maintain a distance specified in the setting ``distance``.
11 luisa
12 -- grid_codes -- Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_ Specifies areas according to the LUISA Base Map codes which are generally eligible for wind turbine placement.
13 -- distance m float Distance to keep from areas specified in ``distance_grid_codes``
14 -- distance_grid_codes -- Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_ Specifies areas according to the LUISA Base Map codes to which wind turbines must maintain a distance specified in the setting ``distance``.
15 natura bool {true, false} Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``.
potential -- One of {'simple', 'conservative'} Method to compute the maximal installable potential for a node; confer :ref:`renewableprofiles`
16 clip_p_max_pu p.u. float To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero.
17 correction_factor -- float Correction factor for capacity factor time series.
18 excluder_resolution m float Resolution on which to perform geographical elibility analysis.

View File

@ -71,7 +71,6 @@ solar_thermal,--,"{true, false}",Add option for using solar thermal to generate
solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations
marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids
methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation.
helmeth,--,"{true, false}",Add option for transforming power into gas using HELMETH (Integrated High-Temperature ELectrolysis and METHanation for Effective Power to Gas Conversion)
coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture
dac,--,"{true, false}",Add option for Direct Air Capture (DAC) dac,--,"{true, false}",Add option for Direct Air Capture (DAC)
co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere.
@ -80,6 +79,8 @@ hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell f
hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs
SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR)
SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC)
regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed.
regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed.
regional_co2 _sequestration_potential,,, regional_co2 _sequestration_potential,,,
-- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP <https://setis.ec.europa.eu/european-co2-storage-database_en>`_. -- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP <https://setis.ec.europa.eu/european-co2-storage-database_en>`_.
-- attribute,--,string,Name of the attribute for the sequestration potential -- attribute,--,string,Name of the attribute for the sequestration potential
@ -92,6 +93,7 @@ co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2
co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites."
,,, ,,,
co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network
co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network
,,, ,,,
cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture
hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally.
@ -108,6 +110,11 @@ electricity_distribution _grid,--,"{true, false}",Add a simplified representatio
electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid
,,, ,,,
electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar
transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links.
-- {carrier},--,str,The carrier of the link.
-- -- efficiency_static,p.u.,float,Length-independent transmission efficiency.
-- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$)
-- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus.
H2_network,--,"{true, false}",Add option for new hydrogen pipelines H2_network,--,"{true, false}",Add option for new hydrogen pipelines
gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation.html#networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation>`_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation.html#networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation>`_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well."
H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen.

1 Unit Values Description
71 solar_cf_correction -- float The correction factor for the value provided by the solar thermal profile calculations
72 marginal_cost_storage currency/MWh float The marginal cost of discharging batteries in distributed grids
73 methanation -- {true, false} Add option for transforming hydrogen and CO2 into methane using methanation.
helmeth -- {true, false} Add option for transforming power into gas using HELMETH (Integrated High-Temperature ELectrolysis and METHanation for Effective Power to Gas Conversion)
74 coal_cc -- {true, false} Add option for coal CHPs with carbon capture
75 dac -- {true, false} Add option for Direct Air Capture (DAC)
76 co2_vent -- {true, false} Add option for vent out CO2 from storages to the atmosphere.
79 hydrogen_turbine -- {true, false} Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs
80 SMR -- {true, false} Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR)
81 SMR CC -- {true, false} Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC)
82 regional_methanol_demand -- {true, false} Spatially resolve methanol demand. Set to true if regional CO2 constraints needed.
83 regional_oil_demand -- {true, false} Spatially resolve oil demand. Set to true if regional CO2 constraints needed.
84 regional_co2 _sequestration_potential
85 -- enable -- {true, false} Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP <https://setis.ec.europa.eu/european-co2-storage-database_en>`_.
86 -- attribute -- string Name of the attribute for the sequestration potential
93 co2_spatial -- {true, false} Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites.
94
95 co2network -- {true, false} Add option for planning a new carbon dioxide transmission network
96 co2_network_cost_factor p.u. float The cost factor for the capital cost of the carbon dioxide transmission network
97
98 cc_fraction -- float The default fraction of CO2 captured with post-combustion capture
99 hydrogen_underground _storage -- {true, false} Add options for storing hydrogen underground. Storage potential depends regionally.
110 electricity_distribution _grid_cost_factor Multiplies the investment cost of the electricity distribution grid
111
112 electricity_grid _connection -- {true, false} Add the cost of electricity grid connection for onshore wind and solar
113 transmission_efficiency Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links.
114 -- {carrier} -- str The carrier of the link.
115 -- -- efficiency_static p.u. float Length-independent transmission efficiency.
116 -- -- efficiency_per_1000km p.u. per 1000 km float Length-dependent transmission efficiency ($\eta^{\text{length}}$)
117 -- -- compression_per_1000km p.u. per 1000 km float Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus.
118 H2_network -- {true, false} Add option for new hydrogen pipelines
119 gas_network -- {true, false} Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm <https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation.html#networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation>`_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well.
120 H2_retrofit -- {true, false} Add option for retrofiting existing pipelines to transport hydrogen.

View File

@ -9,7 +9,7 @@ resource,,,
capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of solar panel placement." capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of solar panel placement."
correction_factor,--,float,"A correction factor for the capacity factor (availability) time series." correction_factor,--,float,"A correction factor for the capacity factor (availability) time series."
corine,--,"Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for solar panel placement." corine,--,"Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for solar panel placement."
luisa,--,"Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_","Specifies areas according to the LUISA Base Map codes which are generally eligible for solar panel placement."
natura,bool,"{true, false}","Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``." natura,bool,"{true, false}","Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``."
potential,--,"One of {'simple', 'conservative'}","Method to compute the maximal installable potential for a node; confer :ref:`renewableprofiles`"
clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero." clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero."
excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis." excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis."

1 Unit Values Description
9 capacity_per_sqkm :math:`MW/km^2` float Allowable density of solar panel placement.
10 correction_factor -- float A correction factor for the capacity factor (availability) time series.
11 corine -- Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_ Specifies areas according to CORINE Land Cover codes which are generally eligible for solar panel placement.
12 luisa -- Any subset of the `LUISA Base Map codes in Annex 1 <https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_ Specifies areas according to the LUISA Base Map codes which are generally eligible for solar panel placement.
13 natura bool {true, false} Switch to exclude `Natura 2000 <https://en.wikipedia.org/wiki/Natura_2000>`_ natural protection areas. Area is excluded if ``true``.
potential -- One of {'simple', 'conservative'} Method to compute the maximal installable potential for a node; confer :ref:`renewableprofiles`
14 clip_p_max_pu p.u. float To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero.
15 excluder_resolution m float Resolution on which to perform geographical elibility analysis.

View File

@ -6,6 +6,7 @@ options,,,
-- skip_iterations,bool,"{'true','false'}","Skip iterating, do not update impedances of branches. Defaults to true." -- skip_iterations,bool,"{'true','false'}","Skip iterating, do not update impedances of branches. Defaults to true."
-- rolling_horizon,bool,"{'true','false'}","Whether to optimize the network in a rolling horizon manner, where the snapshot range is split into slices of size `horizon` which are solved consecutively." -- rolling_horizon,bool,"{'true','false'}","Whether to optimize the network in a rolling horizon manner, where the snapshot range is split into slices of size `horizon` which are solved consecutively."
-- seed,--,int,Random seed for increased deterministic behaviour. -- seed,--,int,Random seed for increased deterministic behaviour.
-- custom_extra_functionality,--,str,Path to a Python file with custom extra functionality code to be injected into the solving rules of the workflow relative to ``rules`` directory.
-- track_iterations,bool,"{'true','false'}",Flag whether to store the intermediate branch capacities and objective function values are recorded for each iteration in ``network.lines['s_nom_opt_X']`` (where ``X`` labels the iteration) -- track_iterations,bool,"{'true','false'}",Flag whether to store the intermediate branch capacities and objective function values are recorded for each iteration in ``network.lines['s_nom_opt_X']`` (where ``X`` labels the iteration)
-- min_iterations,--,int,Minimum number of solving iterations in between which resistance and reactence (``x/r``) are updated for branches according to ``s_nom_opt`` of the previous run. -- min_iterations,--,int,Minimum number of solving iterations in between which resistance and reactence (``x/r``) are updated for branches according to ``s_nom_opt`` of the previous run.
-- max_iterations,--,int,Maximum number of solving iterations in between which resistance and reactence (``x/r``) are updated for branches according to ``s_nom_opt`` of the previous run. -- max_iterations,--,int,Maximum number of solving iterations in between which resistance and reactence (``x/r``) are updated for branches according to ``s_nom_opt`` of the previous run.

1 Unit Values Description
6 -- skip_iterations bool {'true','false'} Skip iterating, do not update impedances of branches. Defaults to true.
7 -- rolling_horizon bool {'true','false'} Whether to optimize the network in a rolling horizon manner, where the snapshot range is split into slices of size `horizon` which are solved consecutively.
8 -- seed -- int Random seed for increased deterministic behaviour.
9 -- custom_extra_functionality -- str Path to a Python file with custom extra functionality code to be injected into the solving rules of the workflow relative to ``rules`` directory.
10 -- track_iterations bool {'true','false'} Flag whether to store the intermediate branch capacities and objective function values are recorded for each iteration in ``network.lines['s_nom_opt_X']`` (where ``X`` labels the iteration)
11 -- min_iterations -- int Minimum number of solving iterations in between which resistance and reactence (``x/r``) are updated for branches according to ``s_nom_opt`` of the previous run.
12 -- max_iterations -- int Maximum number of solving iterations in between which resistance and reactence (``x/r``) are updated for branches according to ``s_nom_opt`` of the previous run.

View File

@ -10,6 +10,44 @@ Release Notes
Upcoming Release Upcoming Release
================ ================
* Distinguish between stored and sequestered CO2. Stored CO2 is stored
overground in tanks and can be used for CCU (e.g. methanolisation).
Sequestered CO2 is stored underground and can no longer be used for CCU. This
distinction is made because storage in tanks is more expensive than
underground storage. The link that connects stored and sequestered CO2 is
unidirectional.
* Increase deployment density of solar to 5.1 MW/sqkm by default.
* Default to full electrification of land transport by 2050.
* Default to approximating transmission losses in HVAC lines
(``transmission_losses: 2``).
* Remove all negative loads on the ``co2 atmosphere`` bus representing emissions
for e.g. fixed fossil demands for transport oil. Instead these are handled
more transparently with a fixed transport oil demand and a link taking care of
the emissions to the ``co2 atmosphere`` bus. This is also a preparation for
endogenous transport optimisation, where demand will be subject to
optimisation (e.g. fuel switching in the transport sector).
* Allow possibility to go from copperplated to regionally resolved methanol and
oil demand with switches ``sector: regional_methanol_demand: true`` and
``sector: regional_oil_demand: true``. This allows nodal/regional CO2
constraints to be applied.
* Process emissions from steam crackers (i.e. naphtha processing for HVC) are now
piped from the consumption link to the process emissions bus where the model
can decide about carbon capture. Previously the process emissions for naphtha
were a fixed load.
* Add option to specify losses for bidirectional links, e.g. pipelines or HVDC
links, in configuration file under ``sector: transmission_efficiency:``. Users
can specify static or length-dependent values as well as a length-dependent
electricity demand for compression, which is implemented as a multi-link to
the local electricity buses. The bidirectional links will then be split into
two unidirectional links with linked capacities.
* Pin ``snakemake`` version to below 8.0.0, as the new version is not yet * Pin ``snakemake`` version to below 8.0.0, as the new version is not yet
supported by ``pypsa-eur``. supported by ``pypsa-eur``.
@ -72,6 +110,11 @@ Upcoming Release
reconnected to the main Ukrainian grid with the configuration option reconnected to the main Ukrainian grid with the configuration option
`reconnect_crimea`. `reconnect_crimea`.
* Add option to reference an additional source file where users can specify
custom ``extra_functionality`` constraints in the configuration file. The
default setting points to an empty hull at
``data/custom_extra_functionality.py``.
* Validate downloads from Zenodo using MD5 checksums. This identifies corrupted * Validate downloads from Zenodo using MD5 checksums. This identifies corrupted
or incomplete downloads. or incomplete downloads.
@ -79,6 +122,21 @@ Upcoming Release
Energy Monitor's `Europe Gas Tracker Energy Monitor's `Europe Gas Tracker
<https://globalenergymonitor.org/projects/europe-gas-tracker>`_. <https://globalenergymonitor.org/projects/europe-gas-tracker>`_.
* Remove HELMETH option.
* Print Irreducible Infeasible Subset (IIS) if model is infeasible. Only for
solvers with IIS support.
* Add option to use `LUISA Base Map
<https://publications.jrc.ec.europa.eu/repository/handle/JRC124621>`_ 50m land
coverage dataset for land eligibility analysis in
:mod:`build_renewable_profiles`. Settings are analogous to the CORINE dataset
but with the key ``luisa:`` in the configuration file. To leverage the
dataset's full advantages, set the excluder resolution to 50m
(``excluder_resolution: 50``). For land category codes, see `Annex 1 of the
technical documentation
<https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_.
**Bugs and Compatibility** **Bugs and Compatibility**
* A bug preventing custom powerplants specified in ``data/custom_powerplants.csv`` was fixed. (https://github.com/PyPSA/pypsa-eur/pull/732) * A bug preventing custom powerplants specified in ``data/custom_powerplants.csv`` was fixed. (https://github.com/PyPSA/pypsa-eur/pull/732)

View File

@ -269,6 +269,11 @@ rule build_renewable_profiles:
if config["renewable"][w.technology]["natura"] if config["renewable"][w.technology]["natura"]
else [] else []
), ),
luisa=lambda w: (
"data/LUISA_basemap_020321_50m.tif"
if config["renewable"][w.technology].get("luisa")
else []
),
gebco=ancient( gebco=ancient(
lambda w: ( lambda w: (
"data/bundle/GEBCO_2014_2D.nc" "data/bundle/GEBCO_2014_2D.nc"

View File

@ -67,8 +67,6 @@ rule build_simplified_population_layouts:
"../scripts/build_clustered_population_layouts.py" "../scripts/build_clustered_population_layouts.py"
if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]:
rule build_gas_network: rule build_gas_network:
input: input:
gas_network="data/gas_network/scigrid-gas/data/IGGIELGN_PipeSegments.geojson", gas_network="data/gas_network/scigrid-gas/data/IGGIELGN_PipeSegments.geojson",
@ -83,6 +81,7 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]:
script: script:
"../scripts/build_gas_network.py" "../scripts/build_gas_network.py"
rule build_gas_input_locations: rule build_gas_input_locations:
input: input:
gem=HTTP.remote( gem=HTTP.remote(
@ -91,13 +90,10 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]:
), ),
entry="data/gas_network/scigrid-gas/data/IGGIELGN_BorderPoints.geojson", entry="data/gas_network/scigrid-gas/data/IGGIELGN_BorderPoints.geojson",
storage="data/gas_network/scigrid-gas/data/IGGIELGN_Storages.geojson", storage="data/gas_network/scigrid-gas/data/IGGIELGN_Storages.geojson",
regions_onshore=RESOURCES regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson",
+ "regions_onshore_elec_s{simpl}_{clusters}.geojson", regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson",
regions_offshore=RESOURCES
+ "regions_offshore_elec_s{simpl}_{clusters}.geojson",
output: output:
gas_input_nodes=RESOURCES gas_input_nodes=RESOURCES + "gas_input_locations_s{simpl}_{clusters}.geojson",
+ "gas_input_locations_s{simpl}_{clusters}.geojson",
gas_input_nodes_simplified=RESOURCES gas_input_nodes_simplified=RESOURCES
+ "gas_input_locations_s{simpl}_{clusters}_simplified.csv", + "gas_input_locations_s{simpl}_{clusters}_simplified.csv",
resources: resources:
@ -109,13 +105,12 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]:
script: script:
"../scripts/build_gas_input_locations.py" "../scripts/build_gas_input_locations.py"
rule cluster_gas_network: rule cluster_gas_network:
input: input:
cleaned_gas_network=RESOURCES + "gas_network.csv", cleaned_gas_network=RESOURCES + "gas_network.csv",
regions_onshore=RESOURCES regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson",
+ "regions_onshore_elec_s{simpl}_{clusters}.geojson", regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson",
regions_offshore=RESOURCES
+ "regions_offshore_elec_s{simpl}_{clusters}.geojson",
output: output:
clustered_gas_network=RESOURCES + "gas_network_elec_s{simpl}_{clusters}.csv", clustered_gas_network=RESOURCES + "gas_network_elec_s{simpl}_{clusters}.csv",
resources: resources:
@ -127,17 +122,6 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]:
script: script:
"../scripts/cluster_gas_network.py" "../scripts/cluster_gas_network.py"
gas_infrastructure = {
**rules.cluster_gas_network.output,
**rules.build_gas_input_locations.output,
}
if not (config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]):
# this is effecively an `else` statement which is however not liked by snakefmt
gas_infrastructure = {}
rule build_heat_demands: rule build_heat_demands:
params: params:
@ -722,7 +706,8 @@ rule prepare_sector_network:
input: input:
**build_retro_cost_output, **build_retro_cost_output,
**build_biomass_transport_costs_output, **build_biomass_transport_costs_output,
**gas_infrastructure, **rules.cluster_gas_network.output,
**rules.build_gas_input_locations.output,
**build_sequestration_potentials_output, **build_sequestration_potentials_output,
network=RESOURCES + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", network=RESOURCES + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc",
energy_totals_name=RESOURCES + "energy_totals.csv", energy_totals_name=RESOURCES + "energy_totals.csv",

View File

@ -2,9 +2,14 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import os, sys import os, sys, glob
helper_source_path = [match for match in glob.glob("**/_helpers.py", recursive=True)]
for path in helper_source_path:
path = os.path.dirname(os.path.abspath(path))
sys.path.insert(0, os.path.abspath(path))
sys.path.insert(0, os.path.abspath("scripts"))
from _helpers import validate_checksum from _helpers import validate_checksum
@ -28,6 +33,13 @@ def memory(w):
return int(factor * (10000 + 195 * int(w.clusters))) return int(factor * (10000 + 195 * int(w.clusters)))
def input_custom_extra_functionality(w):
path = config["solving"]["options"].get("custom_extra_functionality", False)
if path:
return workflow.source_path(path)
return []
# Check if the workflow has access to the internet by trying to access the HEAD of specified url # Check if the workflow has access to the internet by trying to access the HEAD of specified url
def has_internet_access(url="www.zenodo.org") -> bool: def has_internet_access(url="www.zenodo.org") -> bool:
import http.client as http_client import http.client as http_client

View File

@ -162,9 +162,7 @@ if config["enable"]["retrieve"] and config["enable"].get(
"../scripts/retrieve_sector_databundle.py" "../scripts/retrieve_sector_databundle.py"
if config["enable"]["retrieve"] and ( if config["enable"]["retrieve"]:
config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]
):
datafiles = [ datafiles = [
"IGGIELGN_LNGs.geojson", "IGGIELGN_LNGs.geojson",
"IGGIELGN_BorderPoints.geojson", "IGGIELGN_BorderPoints.geojson",
@ -249,6 +247,22 @@ if config["enable"]["retrieve"]:
validate_checksum(output[0], input[0]) validate_checksum(output[0], input[0])
if config["enable"]["retrieve"]:
# Downloading LUISA Base Map for land cover and land use:
# Website: https://ec.europa.eu/jrc/en/luisa
rule retrieve_luisa_land_cover:
input:
HTTP.remote(
"jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/LUISA/EUROPE/Basemaps/LandUse/2018/LATEST/LUISA_basemap_020321_50m.tif",
static=True,
),
output:
"data/LUISA_basemap_020321_50m.tif",
run:
move(input[0], output[0])
if config["enable"]["retrieve"]: if config["enable"]["retrieve"]:
# Some logic to find the correct file URL # Some logic to find the correct file URL
# Sometimes files are released delayed or ahead of schedule, check which file is currently available # Sometimes files are released delayed or ahead of schedule, check which file is currently available

View File

@ -11,6 +11,7 @@ rule solve_network:
co2_sequestration_potential=config["sector"].get( co2_sequestration_potential=config["sector"].get(
"co2_sequestration_potential", 200 "co2_sequestration_potential", 200
), ),
custom_extra_functionality=input_custom_extra_functionality,
input: input:
network=RESOURCES + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", network=RESOURCES + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc",
config=RESULTS + "config.yaml", config=RESULTS + "config.yaml",

View File

@ -88,6 +88,7 @@ rule solve_sector_network_myopic:
co2_sequestration_potential=config["sector"].get( co2_sequestration_potential=config["sector"].get(
"co2_sequestration_potential", 200 "co2_sequestration_potential", 200
), ),
custom_extra_functionality=input_custom_extra_functionality,
input: input:
network=RESULTS network=RESULTS
+ "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",

View File

@ -11,6 +11,7 @@ rule solve_sector_network:
co2_sequestration_potential=config["sector"].get( co2_sequestration_potential=config["sector"].get(
"co2_sequestration_potential", 200 "co2_sequestration_potential", 200
), ),
custom_extra_functionality=input_custom_extra_functionality,
input: input:
network=RESULTS network=RESULTS
+ "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",

View File

@ -118,6 +118,7 @@ rule solve_sector_network_perfect:
co2_sequestration_potential=config["sector"].get( co2_sequestration_potential=config["sector"].get(
"co2_sequestration_potential", 200 "co2_sequestration_potential", 200
), ),
custom_extra_functionality=input_custom_extra_functionality,
input: input:
network=RESULTS network=RESULTS
+ "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc",

View File

@ -120,6 +120,31 @@ def add_brownfield(n, n_p, year):
n.links.loc[new_pipes, "p_nom_min"] = 0.0 n.links.loc[new_pipes, "p_nom_min"] = 0.0
def disable_grid_expansion_if_LV_limit_hit(n):
if not "lv_limit" in n.global_constraints.index:
return
total_expansion = (
n.lines.eval("s_nom_min * length").sum()
+ n.links.query("carrier == 'DC'").eval("p_nom_min * length").sum()
).sum()
lv_limit = n.global_constraints.at["lv_limit", "constant"]
# allow small numerical differences
if lv_limit - total_expansion < 1:
logger.info(f"LV is already reached, disabling expansion and LV limit")
extendable_acs = n.lines.query("s_nom_extendable").index
n.lines.loc[extendable_acs, "s_nom_extendable"] = False
n.lines.loc[extendable_acs, "s_nom"] = n.lines.loc[extendable_acs, "s_nom_min"]
extendable_dcs = n.links.query("carrier == 'DC' and p_nom_extendable").index
n.links.loc[extendable_dcs, "p_nom_extendable"] = False
n.links.loc[extendable_dcs, "p_nom"] = n.links.loc[extendable_dcs, "p_nom_min"]
n.global_constraints.drop("lv_limit", inplace=True)
if __name__ == "__main__": if __name__ == "__main__":
if "snakemake" not in globals(): if "snakemake" not in globals():
from _helpers import mock_snakemake from _helpers import mock_snakemake
@ -150,5 +175,7 @@ if __name__ == "__main__":
add_brownfield(n, n_p, year) add_brownfield(n, n_p, year)
disable_grid_expansion_if_LV_limit_hit(n)
n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards)))
n.export_to_netcdf(snakemake.output[0]) n.export_to_netcdf(snakemake.output[0])

View File

@ -305,7 +305,7 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas
else: else:
bus0 = vars(spatial)[carrier[generator]].nodes bus0 = vars(spatial)[carrier[generator]].nodes
if "EU" not in vars(spatial)[carrier[generator]].locations: if "EU" not in vars(spatial)[carrier[generator]].locations:
bus0 = bus0.intersection(capacity.index + " gas") bus0 = bus0.intersection(capacity.index + " " + carrier[generator])
# check for missing bus # check for missing bus
missing_bus = pd.Index(bus0).difference(n.buses.index) missing_bus = pd.Index(bus0).difference(n.buses.index)

View File

@ -25,10 +25,7 @@ if __name__ == "__main__":
cutout = atlite.Cutout(snakemake.input.cutout) cutout = atlite.Cutout(snakemake.input.cutout)
clustered_regions = ( clustered_regions = (
gpd.read_file(snakemake.input.regions_onshore) gpd.read_file(snakemake.input.regions_onshore).set_index("name").buffer(0)
.set_index("name")
.buffer(0)
.squeeze()
) )
I = cutout.indicatormatrix(clustered_regions) I = cutout.indicatormatrix(clustered_regions)

View File

@ -31,10 +31,7 @@ if __name__ == "__main__":
cutout = atlite.Cutout(snakemake.input.cutout).sel(time=time) cutout = atlite.Cutout(snakemake.input.cutout).sel(time=time)
clustered_regions = ( clustered_regions = (
gpd.read_file(snakemake.input.regions_onshore) gpd.read_file(snakemake.input.regions_onshore).set_index("name").buffer(0)
.set_index("name")
.buffer(0)
.squeeze()
) )
I = cutout.indicatormatrix(clustered_regions) I = cutout.indicatormatrix(clustered_regions)

View File

@ -130,7 +130,7 @@ def build_nodal_distribution_key(hotmaps, regions, countries):
if not facilities.empty: if not facilities.empty:
emissions = facilities["Emissions_ETS_2014"].fillna( emissions = facilities["Emissions_ETS_2014"].fillna(
hotmaps["Emissions_EPRTR_2014"] hotmaps["Emissions_EPRTR_2014"].dropna()
) )
if emissions.sum() == 0: if emissions.sum() == 0:
key = pd.Series(1 / len(facilities), facilities.index) key = pd.Series(1 / len(facilities), facilities.index)

View File

@ -26,20 +26,9 @@ Relevant settings
renewable: renewable:
{technology}: {technology}:
cutout: cutout: corine: luisa: grid_codes: distance: natura: max_depth:
corine: max_shore_distance: min_shore_distance: capacity_per_sqkm:
grid_codes: correction_factor: min_p_max_pu: clip_p_max_pu: resource:
distance:
natura:
max_depth:
max_shore_distance:
min_shore_distance:
capacity_per_sqkm:
correction_factor:
potential:
min_p_max_pu:
clip_p_max_pu:
resource:
.. seealso:: .. seealso::
Documentation of the configuration file ``config/config.yaml`` at Documentation of the configuration file ``config/config.yaml`` at
@ -48,21 +37,37 @@ Relevant settings
Inputs Inputs
------ ------
- ``data/bundle/corine/g250_clc06_V18_5.tif``: `CORINE Land Cover (CLC) <https://land.copernicus.eu/pan-european/corine-land-cover>`_ inventory on `44 classes <https://wiki.openstreetmap.org/wiki/Corine_Land_Cover#Tagging>`_ of land use (e.g. forests, arable land, industrial, urban areas). - ``data/bundle/corine/g250_clc06_V18_5.tif``: `CORINE Land Cover (CLC)
<https://land.copernicus.eu/pan-european/corine-land-cover>`_ inventory on `44
classes <https://wiki.openstreetmap.org/wiki/Corine_Land_Cover#Tagging>`_ of
land use (e.g. forests, arable land, industrial, urban areas) at 100m
resolution.
.. image:: img/corine.png .. image:: img/corine.png
:scale: 33 % :scale: 33 %
- ``data/bundle/GEBCO_2014_2D.nc``: A `bathymetric <https://en.wikipedia.org/wiki/Bathymetry>`_ data set with a global terrain model for ocean and land at 15 arc-second intervals by the `General Bathymetric Chart of the Oceans (GEBCO) <https://www.gebco.net/data_and_products/gridded_bathymetry_data/>`_. - ``data/LUISA_basemap_020321_50m.tif``: `LUISA Base Map
<https://publications.jrc.ec.europa.eu/repository/handle/JRC124621>`_ land
coverage dataset at 50m resolution similar to CORINE. For codes in relation to
CORINE land cover, see `Annex 1 of the technical documentation
<https://publications.jrc.ec.europa.eu/repository/bitstream/JRC124621/technical_report_luisa_basemap_2018_v7_final.pdf>`_.
- ``data/bundle/GEBCO_2014_2D.nc``: A `bathymetric
<https://en.wikipedia.org/wiki/Bathymetry>`_ data set with a global terrain
model for ocean and land at 15 arc-second intervals by the `General
Bathymetric Chart of the Oceans (GEBCO)
<https://www.gebco.net/data_and_products/gridded_bathymetry_data/>`_.
.. image:: img/gebco_2019_grid_image.jpg .. image:: img/gebco_2019_grid_image.jpg
:scale: 50 % :scale: 50 %
**Source:** `GEBCO <https://www.gebco.net/data_and_products/images/gebco_2019_grid_image.jpg>`_ **Source:** `GEBCO
<https://www.gebco.net/data_and_products/images/gebco_2019_grid_image.jpg>`_
- ``resources/natura.tiff``: confer :ref:`natura` - ``resources/natura.tiff``: confer :ref:`natura`
- ``resources/offshore_shapes.geojson``: confer :ref:`shapes` - ``resources/offshore_shapes.geojson``: confer :ref:`shapes`
- ``resources/regions_onshore.geojson``: (if not offshore wind), confer :ref:`busregions` - ``resources/regions_onshore.geojson``: (if not offshore wind), confer
:ref:`busregions`
- ``resources/regions_offshore.geojson``: (if offshore wind), :ref:`busregions` - ``resources/regions_offshore.geojson``: (if offshore wind), :ref:`busregions`
- ``"cutouts/" + params["renewable"][{technology}]['cutout']``: :ref:`cutout` - ``"cutouts/" + params["renewable"][{technology}]['cutout']``: :ref:`cutout`
- ``networks/base.nc``: :ref:`base` - ``networks/base.nc``: :ref:`base`
@ -128,25 +133,26 @@ Description
This script functions at two main spatial resolutions: the resolution of the This script functions at two main spatial resolutions: the resolution of the
network nodes and their `Voronoi cells network nodes and their `Voronoi cells
<https://en.wikipedia.org/wiki/Voronoi_diagram>`_, and the resolution of the <https://en.wikipedia.org/wiki/Voronoi_diagram>`_, and the resolution of the
cutout grid cells for the weather data. Typically the weather data grid is cutout grid cells for the weather data. Typically the weather data grid is finer
finer than the network nodes, so we have to work out the distribution of than the network nodes, so we have to work out the distribution of generators
generators across the grid cells within each Voronoi cell. This is done by across the grid cells within each Voronoi cell. This is done by taking account
taking account of a combination of the available land at each grid cell and the of a combination of the available land at each grid cell and the capacity factor
capacity factor there. there.
First the script computes how much of the technology can be installed at each First the script computes how much of the technology can be installed at each
cutout grid cell and each node using the `GLAES cutout grid cell and each node using the `atlite
<https://github.com/FZJ-IEK3-VSA/glaes>`_ library. This uses the CORINE land use data, <https://github.com/pypsa/atlite>`_ library. This uses the CORINE land use data,
Natura2000 nature reserves and GEBCO bathymetry data. LUISA land use data, Natura2000 nature reserves, GEBCO bathymetry data, and
shipping lanes.
.. image:: img/eligibility.png .. image:: img/eligibility.png
:scale: 50 % :scale: 50 %
:align: center :align: center
To compute the layout of generators in each node's Voronoi cell, the To compute the layout of generators in each node's Voronoi cell, the installable
installable potential in each grid cell is multiplied with the capacity factor potential in each grid cell is multiplied with the capacity factor at each grid
at each grid cell. This is done since we assume more generators are installed cell. This is done since we assume more generators are installed at cells with a
at cells with a higher capacity factor. higher capacity factor.
.. image:: img/offwinddc-gridcell.png .. image:: img/offwinddc-gridcell.png
:scale: 50 % :scale: 50 %
@ -164,20 +170,14 @@ at cells with a higher capacity factor.
:scale: 50 % :scale: 50 %
:align: center :align: center
This layout is then used to compute the generation availability time series This layout is then used to compute the generation availability time series from
from the weather data cutout from ``atlite``. the weather data cutout from ``atlite``.
Two methods are available to compute the maximal installable potential for the The maximal installable potential for the node (`p_nom_max`) is computed by
node (`p_nom_max`): ``simple`` and ``conservative``: adding up the installable potentials of the individual grid cells. If the model
comes close to this limit, then the time series may slightly overestimate
- ``simple`` adds up the installable potentials of the individual grid cells. production since it is assumed the geographical distribution is proportional to
If the model comes close to this limit, then the time series may slightly capacity factor.
overestimate production since it is assumed the geographical distribution is
proportional to capacity factor.
- ``conservative`` assertains the nodal limit by increasing capacities
proportional to the layout until the limit of an individual grid cell is
reached.
""" """
import functools import functools
import logging import logging
@ -210,12 +210,8 @@ if __name__ == "__main__":
resource = params["resource"] # pv panel params / wind turbine params resource = params["resource"] # pv panel params / wind turbine params
correction_factor = params.get("correction_factor", 1.0) correction_factor = params.get("correction_factor", 1.0)
capacity_per_sqkm = params["capacity_per_sqkm"] capacity_per_sqkm = params["capacity_per_sqkm"]
p_nom_max_meth = params.get("potential", "conservative")
snapshots = snakemake.params.snapshots snapshots = snakemake.params.snapshots
if isinstance(params.get("corine", {}), list):
params["corine"] = {"grid_codes": params["corine"]}
if correction_factor != 1.0: if correction_factor != 1.0:
logger.info(f"correction_factor is set as {correction_factor}") logger.info(f"correction_factor is set as {correction_factor}")
@ -241,15 +237,28 @@ if __name__ == "__main__":
if params["natura"]: if params["natura"]:
excluder.add_raster(snakemake.input.natura, nodata=0, allow_no_overlap=True) excluder.add_raster(snakemake.input.natura, nodata=0, allow_no_overlap=True)
corine = params.get("corine", {}) for dataset in ["corine", "luisa"]:
if "grid_codes" in corine: kwargs = {"nodata": 0} if dataset == "luisa" else {}
codes = corine["grid_codes"] settings = params.get(dataset, {})
excluder.add_raster(snakemake.input.corine, codes=codes, invert=True, crs=3035) if not settings:
if corine.get("distance", 0.0) > 0.0: continue
codes = corine["distance_grid_codes"] if dataset == "luisa" and res > 50:
buffer = corine["distance"] logger.info(
"LUISA data is available at 50m resolution, "
f"but coarser {res}m resolution is used."
)
if isinstance(settings, list):
settings = {"grid_codes": settings}
if "grid_codes" in settings:
codes = settings["grid_codes"]
excluder.add_raster( excluder.add_raster(
snakemake.input.corine, codes=codes, buffer=buffer, crs=3035 snakemake.input[dataset], codes=codes, invert=True, crs=3035, **kwargs
)
if settings.get("distance", 0.0) > 0.0:
codes = settings["distance_grid_codes"]
buffer = settings["distance"]
excluder.add_raster(
snakemake.input[dataset], codes=codes, buffer=buffer, crs=3035, **kwargs
) )
if params.get("ship_threshold"): if params.get("ship_threshold"):
@ -278,15 +287,14 @@ if __name__ == "__main__":
snakemake.input.country_shapes, buffer=buffer, invert=True snakemake.input.country_shapes, buffer=buffer, invert=True
) )
kwargs = dict(nprocesses=nprocesses, disable_progressbar=noprogress) logger.info("Calculate landuse availability...")
if noprogress:
logger.info("Calculate landuse availabilities...")
start = time.time() start = time.time()
kwargs = dict(nprocesses=nprocesses, disable_progressbar=noprogress)
availability = cutout.availabilitymatrix(regions, excluder, **kwargs) availability = cutout.availabilitymatrix(regions, excluder, **kwargs)
duration = time.time() - start duration = time.time() - start
logger.info(f"Completed availability calculation ({duration:2.2f}s)") logger.info(f"Completed landuse availability calculation ({duration:2.2f}s)")
else:
availability = cutout.availabilitymatrix(regions, excluder, **kwargs)
# For Moldova and Ukraine: Overwrite parts not covered by Corine with # For Moldova and Ukraine: Overwrite parts not covered by Corine with
# externally determined available areas # externally determined available areas
@ -305,8 +313,19 @@ if __name__ == "__main__":
func = getattr(cutout, resource.pop("method")) func = getattr(cutout, resource.pop("method"))
if client is not None: if client is not None:
resource["dask_kwargs"] = {"scheduler": client} resource["dask_kwargs"] = {"scheduler": client}
logger.info("Calculate average capacity factor...")
start = time.time()
capacity_factor = correction_factor * func(capacity_factor=True, **resource) capacity_factor = correction_factor * func(capacity_factor=True, **resource)
layout = capacity_factor * area * capacity_per_sqkm layout = capacity_factor * area * capacity_per_sqkm
duration = time.time() - start
logger.info(f"Completed average capacity factor calculation ({duration:2.2f}s)")
logger.info("Calculate weighted capacity factor time series...")
start = time.time()
profile, capacities = func( profile, capacities = func(
matrix=availability.stack(spatial=["y", "x"]), matrix=availability.stack(spatial=["y", "x"]),
layout=layout, layout=layout,
@ -316,18 +335,14 @@ if __name__ == "__main__":
**resource, **resource,
) )
logger.info(f"Calculating maximal capacity per bus (method '{p_nom_max_meth}')") duration = time.time() - start
if p_nom_max_meth == "simple": logger.info(
p_nom_max = capacity_per_sqkm * availability @ area f"Completed weighted capacity factor time series calculation ({duration:2.2f}s)"
elif p_nom_max_meth == "conservative":
max_cap_factor = capacity_factor.where(availability != 0).max(["x", "y"])
p_nom_max = capacities / max_cap_factor
else:
raise AssertionError(
'Config key `potential` should be one of "simple" '
f'(default) or "conservative", not "{p_nom_max_meth}"'
) )
logger.info(f"Calculating maximal capacity per bus")
p_nom_max = capacity_per_sqkm * availability @ area
logger.info("Calculate average distances.") logger.info("Calculate average distances.")
layoutmatrix = (layout * availability).stack(spatial=["y", "x"]) layoutmatrix = (layout * availability).stack(spatial=["y", "x"])

View File

@ -42,8 +42,8 @@ Description
""" """
import logging import logging
import os
import zipfile import zipfile
from pathlib import Path
import rioxarray import rioxarray
from _helpers import configure_logging from _helpers import configure_logging
@ -62,11 +62,13 @@ if __name__ == "__main__":
xs, Xs, ys, Ys = zip(*(determine_cutout_xXyY(cutout) for cutout in cutouts)) xs, Xs, ys, Ys = zip(*(determine_cutout_xXyY(cutout) for cutout in cutouts))
with zipfile.ZipFile(snakemake.input.ship_density) as zip_f: with zipfile.ZipFile(snakemake.input.ship_density) as zip_f:
zip_f.extract("shipdensity_global.tif") resources = Path(snakemake.output[0]).parent
with rioxarray.open_rasterio("shipdensity_global.tif") as ship_density: fn = "shipdensity_global.tif"
zip_f.extract(fn, resources)
with rioxarray.open_rasterio(resources / fn) as ship_density:
ship_density = ship_density.drop_vars(["band"]).sel( ship_density = ship_density.drop_vars(["band"]).sel(
x=slice(min(xs), max(Xs)), y=slice(max(Ys), min(ys)) x=slice(min(xs), max(Xs)), y=slice(max(Ys), min(ys))
) )
ship_density.rio.to_raster(snakemake.output[0]) ship_density.rio.to_raster(snakemake.output[0])
os.remove("shipdensity_global.tif") (resources / fn).unlink()

View File

@ -33,10 +33,7 @@ if __name__ == "__main__":
cutout = atlite.Cutout(snakemake.input.cutout).sel(time=time) cutout = atlite.Cutout(snakemake.input.cutout).sel(time=time)
clustered_regions = ( clustered_regions = (
gpd.read_file(snakemake.input.regions_onshore) gpd.read_file(snakemake.input.regions_onshore).set_index("name").buffer(0)
.set_index("name")
.buffer(0)
.squeeze()
) )
I = cutout.indicatormatrix(clustered_regions) I = cutout.indicatormatrix(clustered_regions)

View File

@ -31,10 +31,7 @@ if __name__ == "__main__":
cutout = atlite.Cutout(snakemake.input.cutout).sel(time=time) cutout = atlite.Cutout(snakemake.input.cutout).sel(time=time)
clustered_regions = ( clustered_regions = (
gpd.read_file(snakemake.input.regions_onshore) gpd.read_file(snakemake.input.regions_onshore).set_index("name").buffer(0)
.set_index("name")
.buffer(0)
.squeeze()
) )
I = cutout.indicatormatrix(clustered_regions) I = cutout.indicatormatrix(clustered_regions)

View File

@ -31,7 +31,7 @@ def rename_techs_tyndp(tech):
tech = rename_techs(tech) tech = rename_techs(tech)
if "heat pump" in tech or "resistive heater" in tech: if "heat pump" in tech or "resistive heater" in tech:
return "power-to-heat" return "power-to-heat"
elif tech in ["H2 Electrolysis", "methanation", "helmeth", "H2 liquefaction"]: elif tech in ["H2 Electrolysis", "methanation", "H2 liquefaction"]:
return "power-to-gas" return "power-to-gas"
elif tech == "H2": elif tech == "H2":
return "H2 storage" return "H2 storage"
@ -495,7 +495,7 @@ def plot_ch4_map(network):
# make a fake MultiIndex so that area is correct for legend # make a fake MultiIndex so that area is correct for legend
fossil_gas.index = pd.MultiIndex.from_product([fossil_gas.index, ["fossil gas"]]) fossil_gas.index = pd.MultiIndex.from_product([fossil_gas.index, ["fossil gas"]])
methanation_i = n.links[n.links.carrier.isin(["helmeth", "Sabatier"])].index methanation_i = n.links.query("carrier == 'Sabatier'").index
methanation = ( methanation = (
abs( abs(
n.links_t.p1.loc[:, methanation_i].mul( n.links_t.p1.loc[:, methanation_i].mul(

View File

@ -121,7 +121,6 @@ preferred_order = pd.Index(
"gas boiler", "gas boiler",
"gas", "gas",
"natural gas", "natural gas",
"helmeth",
"methanation", "methanation",
"ammonia", "ammonia",
"hydrogen storage", "hydrogen storage",

View File

@ -102,6 +102,9 @@ def define_spatial(nodes, options):
spatial.gas.biogas = ["EU biogas"] spatial.gas.biogas = ["EU biogas"]
spatial.gas.industry = ["gas for industry"] spatial.gas.industry = ["gas for industry"]
spatial.gas.biogas_to_gas = ["EU biogas to gas"] spatial.gas.biogas_to_gas = ["EU biogas to gas"]
if options.get("biomass_spatial", options["biomass_transport"]):
spatial.gas.biogas_to_gas_cc = nodes + " biogas to gas CC"
else:
spatial.gas.biogas_to_gas_cc = ["EU biogas to gas CC"] spatial.gas.biogas_to_gas_cc = ["EU biogas to gas CC"]
if options.get("co2_spatial", options["co2network"]): if options.get("co2_spatial", options["co2network"]):
spatial.gas.industry_cc = nodes + " gas for industry CC" spatial.gas.industry_cc = nodes + " gas for industry CC"
@ -129,15 +132,43 @@ def define_spatial(nodes, options):
spatial.h2.locations = nodes spatial.h2.locations = nodes
# methanol # methanol
# beware: unlike other carriers, uses locations rather than locations+carriername
# this allows to avoid separation between nodes and locations
spatial.methanol = SimpleNamespace() spatial.methanol = SimpleNamespace()
spatial.methanol.nodes = ["EU methanol"] spatial.methanol.nodes = ["EU methanol"]
spatial.methanol.locations = ["EU"] spatial.methanol.locations = ["EU"]
if options["regional_methanol_demand"]:
spatial.methanol.demand_locations = nodes
spatial.methanol.shipping = nodes + " shipping methanol"
else:
spatial.methanol.demand_locations = ["EU"]
spatial.methanol.shipping = ["EU shipping methanol"]
# oil # oil
spatial.oil = SimpleNamespace() spatial.oil = SimpleNamespace()
spatial.oil.nodes = ["EU oil"] spatial.oil.nodes = ["EU oil"]
spatial.oil.locations = ["EU"] spatial.oil.locations = ["EU"]
if options["regional_oil_demand"]:
spatial.oil.demand_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"
spatial.oil.land_transport = nodes + " land transport oil"
else:
spatial.oil.demand_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"]
spatial.oil.land_transport = ["EU land transport oil"]
# uranium # uranium
spatial.uranium = SimpleNamespace() spatial.uranium = SimpleNamespace()
spatial.uranium.nodes = ["EU uranium"] spatial.uranium.nodes = ["EU uranium"]
@ -258,6 +289,8 @@ def build_carbon_budget(o, input_eurostat, fn, emissions_scope, report_year):
) )
planning_horizons = snakemake.params.planning_horizons planning_horizons = snakemake.params.planning_horizons
if not isinstance(planning_horizons, list):
planning_horizons = [planning_horizons]
t_0 = planning_horizons[0] t_0 = planning_horizons[0]
if "be" in o: if "be" in o:
@ -519,7 +552,7 @@ def patch_electricity_network(n):
n.loads_t.p_set.rename(lambda x: x.strip(), axis=1, inplace=True) n.loads_t.p_set.rename(lambda x: x.strip(), axis=1, inplace=True)
def add_co2_tracking(n, options): def add_co2_tracking(n, costs, options):
# minus sign because opposite to how fossil fuels used: # minus sign because opposite to how fossil fuels used:
# CH4 burning puts CH4 down, atmosphere up # CH4 burning puts CH4 down, atmosphere up
n.add("Carrier", "co2", co2_emissions=-1.0) n.add("Carrier", "co2", co2_emissions=-1.0)
@ -537,7 +570,7 @@ def add_co2_tracking(n, options):
bus="co2 atmosphere", bus="co2 atmosphere",
) )
# this tracks CO2 stored, e.g. underground # add CO2 tanks
n.madd( n.madd(
"Bus", "Bus",
spatial.co2.nodes, spatial.co2.nodes,
@ -546,6 +579,39 @@ def add_co2_tracking(n, options):
unit="t_co2", unit="t_co2",
) )
n.madd(
"Store",
spatial.co2.nodes,
e_nom_extendable=True,
capital_cost=costs.at["CO2 storage tank", "fixed"],
carrier="co2 stored",
e_cyclic=True,
bus=spatial.co2.nodes,
)
n.add("Carrier", "co2 stored")
# this tracks CO2 sequestered, e.g. underground
sequestration_buses = pd.Index(spatial.co2.nodes).str.replace(
" stored", " sequestered"
)
n.madd(
"Bus",
sequestration_buses,
location=spatial.co2.locations,
carrier="co2 sequestered",
unit="t_co2",
)
n.madd(
"Link",
sequestration_buses,
bus0=spatial.co2.nodes,
bus1=sequestration_buses,
carrier="co2 sequestered",
efficiency=1.0,
p_nom_extendable=True,
)
if options["regional_co2_sequestration_potential"]["enable"]: if options["regional_co2_sequestration_potential"]["enable"]:
upper_limit = ( upper_limit = (
options["regional_co2_sequestration_potential"]["max_size"] * 1e3 options["regional_co2_sequestration_potential"]["max_size"] * 1e3
@ -561,22 +627,22 @@ def add_co2_tracking(n, options):
.mul(1e6) .mul(1e6)
/ annualiser / annualiser
) # t ) # t
e_nom_max = e_nom_max.rename(index=lambda x: x + " co2 stored") e_nom_max = e_nom_max.rename(index=lambda x: x + " co2 sequestered")
else: else:
e_nom_max = np.inf e_nom_max = np.inf
n.madd( n.madd(
"Store", "Store",
spatial.co2.nodes, sequestration_buses,
e_nom_extendable=True, e_nom_extendable=True,
e_nom_max=e_nom_max, e_nom_max=e_nom_max,
capital_cost=options["co2_sequestration_cost"], capital_cost=options["co2_sequestration_cost"],
carrier="co2 stored", bus=sequestration_buses,
bus=spatial.co2.nodes,
lifetime=options["co2_sequestration_lifetime"], lifetime=options["co2_sequestration_lifetime"],
carrier="co2 sequestered",
) )
n.add("Carrier", "co2 stored") n.add("Carrier", "co2 sequestered")
if options["co2_vent"]: if options["co2_vent"]:
n.madd( n.madd(
@ -605,6 +671,8 @@ def add_co2_network(n, costs):
* co2_links.length * co2_links.length
) )
capital_cost = cost_onshore + cost_submarine capital_cost = cost_onshore + cost_submarine
cost_factor = snakemake.config["sector"]["co2_network_cost_factor"]
capital_cost *= cost_factor
n.madd( n.madd(
"Link", "Link",
@ -1369,23 +1437,6 @@ def add_storage_and_grids(n, costs):
lifetime=costs.at["methanation", "lifetime"], lifetime=costs.at["methanation", "lifetime"],
) )
if options["helmeth"]:
n.madd(
"Link",
spatial.nodes,
suffix=" helmeth",
bus0=nodes,
bus1=spatial.gas.nodes,
bus2=spatial.co2.nodes,
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"],
)
if options.get("coal_cc"): if options.get("coal_cc"):
n.madd( n.madd(
"Link", "Link",
@ -1485,8 +1536,8 @@ def add_land_transport(n, costs):
n.madd( n.madd(
"Bus", "Bus",
nodes, nodes,
location=nodes,
suffix=" EV battery", suffix=" EV battery",
location=nodes,
carrier="Li ion", carrier="Li ion",
unit="MWh_el", unit="MWh_el",
) )
@ -1578,29 +1629,42 @@ def add_land_transport(n, costs):
ice_efficiency = options["transport_internal_combustion_efficiency"] ice_efficiency = options["transport_internal_combustion_efficiency"]
n.madd( p_set_land_transport_oil = (
"Load",
nodes,
suffix=" land transport oil",
bus=spatial.oil.nodes,
carrier="land transport oil",
p_set=ice_share / ice_efficiency * transport[nodes],
)
co2 = (
ice_share ice_share
/ ice_efficiency / ice_efficiency
* transport[nodes].sum().sum() * transport[nodes].rename(columns=lambda x: x + " land transport oil")
/ nhours
* costs.at["oil", "CO2 intensity"]
) )
n.add( if not options["regional_oil_demand"]:
p_set_land_transport_oil = p_set_land_transport_oil.sum(axis=1).to_frame(
name="EU land transport oil"
)
n.madd(
"Bus",
spatial.oil.land_transport,
location=spatial.oil.demand_locations,
carrier="land transport oil",
unit="land transport",
)
n.madd(
"Load", "Load",
"land transport oil emissions", spatial.oil.land_transport,
bus="co2 atmosphere", bus=spatial.oil.land_transport,
carrier="land transport oil emissions", carrier="land transport oil",
p_set=-co2, p_set=p_set_land_transport_oil,
)
n.madd(
"Link",
spatial.oil.land_transport,
bus0=spatial.oil.nodes,
bus1=spatial.oil.land_transport,
bus2="co2 atmosphere",
carrier="land transport oil",
efficiency2=costs.at["oil", "CO2 intensity"],
p_nom_extendable=True,
) )
@ -2198,13 +2262,12 @@ def add_biomass(n, costs):
# Assuming for costs that the CO2 from upgrading is pure, such as in amine scrubbing. I.e., with and without CC is # Assuming for costs that the CO2 from upgrading is pure, such as in amine scrubbing. I.e., with and without CC is
# equivalent. Adding biomass CHP capture because biogas is often small-scale and decentral so further # equivalent. Adding biomass CHP capture because biogas is often small-scale and decentral so further
# from e.g. CO2 grid or buyers. This is a proxy for the added cost for e.g. a raw biogas pipeline to a central upgrading facility # from e.g. CO2 grid or buyers. This is a proxy for the added cost for e.g. a raw biogas pipeline to a central upgrading facility
n.madd( n.madd(
"Link", "Link",
spatial.gas.biogas_to_gas_cc, spatial.gas.biogas_to_gas_cc,
bus0=spatial.gas.biogas, bus0=spatial.gas.biogas,
bus1=spatial.gas.nodes, bus1=spatial.gas.nodes,
bus2="co2 stored", bus2=spatial.co2.nodes,
bus3="co2 atmosphere", bus3="co2 atmosphere",
carrier="biogas to gas CC", carrier="biogas to gas CC",
capital_cost=costs.at["biogas CC", "fixed"] capital_cost=costs.at["biogas CC", "fixed"]
@ -2271,6 +2334,14 @@ def add_biomass(n, costs):
marginal_cost=costs.at["solid biomass", "fuel"] marginal_cost=costs.at["solid biomass", "fuel"]
+ bus_transport_costs * average_distance, + bus_transport_costs * average_distance,
) )
n.add(
"GlobalConstraint",
"biomass limit",
carrier_attribute="solid biomass",
sense="<=",
constant=biomass_potentials["solid biomass"].sum(),
type="operational_limit",
)
# AC buses with district heating # AC buses with district heating
urban_central = n.buses.index[n.buses.carrier == "urban central heat"] urban_central = n.buses.index[n.buses.carrier == "urban central heat"]
@ -2483,9 +2554,14 @@ def add_industry(n, costs):
efficiency=1.0, efficiency=1.0,
) )
if len(spatial.biomass.industry_cc) <= 1 and len(spatial.co2.nodes) > 1:
link_names = nodes + " " + spatial.biomass.industry_cc
else:
link_names = spatial.biomass.industry_cc
n.madd( n.madd(
"Link", "Link",
spatial.biomass.industry_cc, link_names,
bus0=spatial.biomass.nodes, bus0=spatial.biomass.nodes,
bus1=spatial.biomass.industry, bus1=spatial.biomass.industry,
bus2="co2 atmosphere", bus2="co2 atmosphere",
@ -2675,48 +2751,44 @@ def add_industry(n, costs):
efficiency = ( efficiency = (
options["shipping_oil_efficiency"] / options["shipping_methanol_efficiency"] options["shipping_oil_efficiency"] / options["shipping_methanol_efficiency"]
) )
p_set_methanol = shipping_methanol_share * p_set.sum() * efficiency
p_set_methanol = (
shipping_methanol_share
* p_set.rename(lambda x: x + " shipping methanol")
* efficiency
)
if not options["regional_methanol_demand"]:
p_set_methanol = p_set_methanol.sum()
n.madd(
"Bus",
spatial.methanol.shipping,
location=spatial.methanol.demand_locations,
carrier="shipping methanol",
unit="MWh_LHV",
)
n.madd( n.madd(
"Load", "Load",
spatial.methanol.nodes, spatial.methanol.shipping,
suffix=" shipping methanol", bus=spatial.methanol.shipping,
bus=spatial.methanol.nodes,
carrier="shipping methanol", carrier="shipping methanol",
p_set=p_set_methanol, p_set=p_set_methanol,
) )
# 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
co2 = p_set_methanol / options["MWh_MeOH_per_tCO2"]
n.add(
"Load",
"shipping methanol emissions",
bus="co2 atmosphere",
carrier="shipping methanol emissions",
p_set=-co2,
)
if shipping_oil_share:
p_set_oil = shipping_oil_share * p_set.sum()
n.madd( n.madd(
"Load", "Link",
spatial.oil.nodes, spatial.methanol.shipping,
suffix=" shipping oil", bus0=spatial.methanol.nodes,
bus=spatial.oil.nodes, bus1=spatial.methanol.shipping,
carrier="shipping oil", bus2="co2 atmosphere",
p_set=p_set_oil, carrier="shipping methanol",
) p_nom_extendable=True,
efficiency2=1
co2 = p_set_oil * costs.at["oil", "CO2 intensity"] / options[
"MWh_MeOH_per_tCO2"
n.add( ], # 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
"Load",
"shipping oil emissions",
bus="co2 atmosphere",
carrier="shipping oil emissions",
p_set=-co2,
) )
if "oil" not in n.buses.carrier.unique(): if "oil" not in n.buses.carrier.unique():
@ -2732,7 +2804,8 @@ def add_industry(n, costs):
# could correct to e.g. 0.001 EUR/kWh * annuity and O&M # could correct to e.g. 0.001 EUR/kWh * annuity and O&M
n.madd( n.madd(
"Store", "Store",
[oil_bus + " Store" for oil_bus in spatial.oil.nodes], spatial.oil.nodes,
suffix=" Store",
bus=spatial.oil.nodes, bus=spatial.oil.nodes,
e_nom_extendable=True, e_nom_extendable=True,
e_cyclic=True, e_cyclic=True,
@ -2749,6 +2822,39 @@ def add_industry(n, costs):
marginal_cost=costs.at["oil", "fuel"], marginal_cost=costs.at["oil", "fuel"],
) )
if shipping_oil_share:
p_set_oil = shipping_oil_share * p_set.rename(lambda x: x + " shipping oil")
if not options["regional_oil_demand"]:
p_set_oil = p_set_oil.sum()
n.madd(
"Bus",
spatial.oil.shipping,
location=spatial.oil.demand_locations,
carrier="shipping oil",
unit="MWh_LHV",
)
n.madd(
"Load",
spatial.oil.shipping,
bus=spatial.oil.shipping,
carrier="shipping oil",
p_set=p_set_oil,
)
n.madd(
"Link",
spatial.oil.shipping,
bus0=spatial.oil.nodes,
bus1=spatial.oil.shipping,
bus2="co2 atmosphere",
carrier="shipping oil",
p_nom_extendable=True,
efficiency2=costs.at["oil", "CO2 intensity"],
)
if options["oil_boilers"]: if options["oil_boilers"]:
nodes_heat = create_nodes_for_heat_sector()[0] nodes_heat = create_nodes_for_heat_sector()[0]
@ -2792,53 +2898,101 @@ def add_industry(n, costs):
lifetime=costs.at["Fischer-Tropsch", "lifetime"], lifetime=costs.at["Fischer-Tropsch", "lifetime"],
) )
# naphtha
demand_factor = options.get("HVC_demand_factor", 1) demand_factor = options.get("HVC_demand_factor", 1)
p_set = demand_factor * industrial_demand.loc[nodes, "naphtha"].sum() / nhours
if demand_factor != 1: if demand_factor != 1:
logger.warning(f"Changing HVC demand by {demand_factor*100-100:+.2f}%.") logger.warning(f"Changing HVC demand by {demand_factor*100-100:+.2f}%.")
n.madd( p_set_plastics = (
"Load",
["naphtha for industry"],
bus=spatial.oil.nodes,
carrier="naphtha for industry",
p_set=p_set,
)
demand_factor = options.get("aviation_demand_factor", 1)
all_aviation = ["total international aviation", "total domestic aviation"]
p_set = (
demand_factor demand_factor
* pop_weighted_energy_totals.loc[nodes, all_aviation].sum(axis=1).sum() * industrial_demand.loc[nodes, "naphtha"].rename(
* 1e6 lambda x: x + " naphtha for industry"
)
/ nhours / nhours
) )
if not options["regional_oil_demand"]:
p_set_plastics = p_set_plastics.sum()
n.madd(
"Bus",
spatial.oil.naphtha,
location=spatial.oil.demand_locations,
carrier="naphtha for industry",
unit="MWh_LHV",
)
n.madd(
"Load",
spatial.oil.naphtha,
bus=spatial.oil.naphtha,
carrier="naphtha for industry",
p_set=p_set_plastics,
)
# some CO2 from naphtha are process emissions from steam cracker
# rest of CO2 released to atmosphere either in waste-to-energy or decay
process_co2_per_naphtha = (
industrial_demand.loc[nodes, "process emission from feedstock"].sum()
/ industrial_demand.loc[nodes, "naphtha"].sum()
)
emitted_co2_per_naphtha = costs.at["oil", "CO2 intensity"] - process_co2_per_naphtha
n.madd(
"Link",
spatial.oil.naphtha,
bus0=spatial.oil.nodes,
bus1=spatial.oil.naphtha,
bus2="co2 atmosphere",
bus3=spatial.co2.process_emissions,
carrier="naphtha for industry",
p_nom_extendable=True,
efficiency2=emitted_co2_per_naphtha,
efficiency3=process_co2_per_naphtha,
)
# aviation
demand_factor = options.get("aviation_demand_factor", 1)
if demand_factor != 1: if demand_factor != 1:
logger.warning(f"Changing aviation demand by {demand_factor*100-100:+.2f}%.") logger.warning(f"Changing aviation demand by {demand_factor*100-100:+.2f}%.")
all_aviation = ["total international aviation", "total domestic aviation"]
p_set = (
demand_factor
* pop_weighted_energy_totals.loc[nodes, all_aviation].sum(axis=1)
* 1e6
/ nhours
).rename(lambda x: x + " kerosene for aviation")
if not options["regional_oil_demand"]:
p_set = p_set.sum()
n.madd(
"Bus",
spatial.oil.kerosene,
location=spatial.oil.demand_locations,
carrier="kerosene for aviation",
unit="MWh_LHV",
)
n.madd( n.madd(
"Load", "Load",
["kerosene for aviation"], spatial.oil.kerosene,
bus=spatial.oil.nodes, bus=spatial.oil.kerosene,
carrier="kerosene for aviation", carrier="kerosene for aviation",
p_set=p_set, p_set=p_set,
) )
# NB: CO2 gets released again to atmosphere when plastics decay or kerosene is burned n.madd(
# except for the process emissions when naphtha is used for petrochemicals, which can be captured with other industry process emissions "Link",
# tco2 per hour spatial.oil.kerosene,
co2_release = ["naphtha for industry", "kerosene for aviation"] bus0=spatial.oil.nodes,
co2 = ( bus1=spatial.oil.kerosene,
n.loads.loc[co2_release, "p_set"].sum() * costs.at["oil", "CO2 intensity"] bus2="co2 atmosphere",
- industrial_demand.loc[nodes, "process emission from feedstock"].sum() / nhours carrier="kerosene for aviation",
) p_nom_extendable=True,
efficiency2=costs.at["oil", "CO2 intensity"],
n.add(
"Load",
"oil emissions",
bus="co2 atmosphere",
carrier="oil emissions",
p_set=-co2,
) )
# TODO simplify bus expression # TODO simplify bus expression
@ -2889,19 +3043,16 @@ def add_industry(n, costs):
unit="t_co2", unit="t_co2",
) )
sel = ["process emission", "process emission from feedstock"]
if options["co2_spatial"] or options["co2network"]: if options["co2_spatial"] or options["co2network"]:
p_set = ( p_set = (
-industrial_demand.loc[nodes, sel] -industrial_demand.loc[nodes, "process emission"].rename(
.sum(axis=1) index=lambda x: x + " process emissions"
.rename(index=lambda x: x + " process emissions") )
/ nhours / nhours
) )
else: else:
p_set = -industrial_demand.loc[nodes, sel].sum(axis=1).sum() / nhours p_set = -industrial_demand.loc[nodes, "process emission"].sum() / nhours
# 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
n.madd( n.madd(
"Load", "Load",
spatial.co2.process_emissions, spatial.co2.process_emissions,
@ -3107,9 +3258,9 @@ def add_agriculture(n, costs):
f"Total agriculture machinery shares sum up to {total_share:.2%}, corresponding to increased or decreased demand assumptions." f"Total agriculture machinery shares sum up to {total_share:.2%}, corresponding to increased or decreased demand assumptions."
) )
machinery_nodal_energy = pop_weighted_energy_totals.loc[ machinery_nodal_energy = (
nodes, "total agriculture machinery" pop_weighted_energy_totals.loc[nodes, "total agriculture machinery"] * 1e6
] )
if electric_share > 0: if electric_share > 0:
efficiency_gain = ( efficiency_gain = (
@ -3123,36 +3274,44 @@ def add_agriculture(n, costs):
suffix=" agriculture machinery electric", suffix=" agriculture machinery electric",
bus=nodes, bus=nodes,
carrier="agriculture machinery electric", carrier="agriculture machinery electric",
p_set=electric_share p_set=electric_share / efficiency_gain * machinery_nodal_energy / nhours,
/ efficiency_gain
* machinery_nodal_energy
* 1e6
/ nhours,
) )
if oil_share > 0: if oil_share > 0:
p_set = (
oil_share
* machinery_nodal_energy.rename(lambda x: x + " agriculture machinery oil")
/ nhours
)
if not options["regional_oil_demand"]:
p_set = p_set.sum()
n.madd(
"Bus",
spatial.oil.agriculture_machinery,
location=spatial.oil.demand_locations,
carrier="agriculture machinery oil",
unit="MWh_LHV",
)
n.madd( n.madd(
"Load", "Load",
["agriculture machinery oil"], spatial.oil.agriculture_machinery,
bus=spatial.oil.nodes, bus=spatial.oil.agriculture_machinery,
carrier="agriculture machinery oil", carrier="agriculture machinery oil",
p_set=oil_share * machinery_nodal_energy.sum() * 1e6 / nhours, p_set=p_set,
) )
co2 = ( n.madd(
oil_share "Link",
* machinery_nodal_energy.sum() spatial.oil.agriculture_machinery,
* 1e6 bus0=spatial.oil.nodes,
/ nhours bus1=spatial.oil.agriculture_machinery,
* costs.at["oil", "CO2 intensity"] bus2="co2 atmosphere",
) carrier="agriculture machinery oil",
p_nom_extendable=True,
n.add( efficiency2=costs.at["oil", "CO2 intensity"],
"Load",
"agriculture machinery oil emissions",
bus="co2 atmosphere",
carrier="agriculture machinery oil emissions",
p_set=-co2,
) )
@ -3175,7 +3334,8 @@ def remove_h2_network(n):
def maybe_adjust_costs_and_potentials(n, opts): def maybe_adjust_costs_and_potentials(n, opts):
for o in opts: for o in opts:
if "+" not in o: flags = ["+e", "+p", "+m"]
if all(flag not in o for flag in flags):
continue continue
oo = o.split("+") oo = o.split("+")
carrier_list = np.hstack( carrier_list = np.hstack(
@ -3221,24 +3381,24 @@ def limit_individual_line_extension(n, maxext):
aggregate_dict = { aggregate_dict = {
"p_nom": "sum", "p_nom": pd.Series.sum,
"s_nom": "sum", "s_nom": pd.Series.sum,
"v_nom": "max", "v_nom": "max",
"v_mag_pu_max": "min", "v_mag_pu_max": "min",
"v_mag_pu_min": "max", "v_mag_pu_min": "max",
"p_nom_max": "sum", "p_nom_max": pd.Series.sum,
"s_nom_max": "sum", "s_nom_max": pd.Series.sum,
"p_nom_min": "sum", "p_nom_min": pd.Series.sum,
"s_nom_min": "sum", "s_nom_min": pd.Series.sum,
"v_ang_min": "max", "v_ang_min": "max",
"v_ang_max": "min", "v_ang_max": "min",
"terrain_factor": "mean", "terrain_factor": "mean",
"num_parallel": "sum", "num_parallel": "sum",
"p_set": "sum", "p_set": "sum",
"e_initial": "sum", "e_initial": "sum",
"e_nom": "sum", "e_nom": pd.Series.sum,
"e_nom_max": "sum", "e_nom_max": pd.Series.sum,
"e_nom_min": "sum", "e_nom_min": pd.Series.sum,
"state_of_charge_initial": "sum", "state_of_charge_initial": "sum",
"state_of_charge_set": "sum", "state_of_charge_set": "sum",
"inflow": "sum", "inflow": "sum",
@ -3300,13 +3460,11 @@ def cluster_heat_buses(n):
pnl = c.pnl pnl = c.pnl
agg = define_clustering(pd.Index(pnl.keys()), aggregate_dict) agg = define_clustering(pd.Index(pnl.keys()), aggregate_dict)
for k in pnl.keys(): for k in pnl.keys():
pnl[k].rename(
columns=lambda x: x.replace("residential ", "").replace( def renamer(s):
"services ", "" return s.replace("residential ", "").replace("services ", "")
),
inplace=True, pnl[k] = pnl[k].groupby(renamer, axis=1).agg(agg[k], **agg_group_kwargs)
)
pnl[k] = pnl[k].groupby(level=0, axis=1).agg(agg[k], **agg_group_kwargs)
# remove unclustered assets of service/residential # remove unclustered assets of service/residential
to_drop = c.df.index.difference(df.index) to_drop = c.df.index.difference(df.index)
@ -3370,6 +3528,7 @@ def apply_time_segmentation(
sn_weightings = pd.Series( sn_weightings = pd.Series(
weightings, index=snapshots, name="weightings", dtype="float64" weightings, index=snapshots, name="weightings", dtype="float64"
) )
logger.info(f"Distribution of snapshot durations:\n{weightings.value_counts()}")
n.set_snapshots(sn_weightings.index) n.set_snapshots(sn_weightings.index)
n.snapshot_weightings = n.snapshot_weightings.mul(sn_weightings, axis=0) n.snapshot_weightings = n.snapshot_weightings.mul(sn_weightings, axis=0)
@ -3411,6 +3570,57 @@ def set_temporal_aggregation(n, opts, solver_name):
return n return n
def lossy_bidirectional_links(n, carrier, efficiencies={}):
"Split bidirectional links into two unidirectional links to include transmission losses."
carrier_i = n.links.query("carrier == @carrier").index
if (
not any((v != 1.0) or (v >= 0) for v in efficiencies.values())
or carrier_i.empty
):
return
efficiency_static = efficiencies.get("efficiency_static", 1)
efficiency_per_1000km = efficiencies.get("efficiency_per_1000km", 1)
compression_per_1000km = efficiencies.get("compression_per_1000km", 0)
logger.info(
f"Specified losses for {carrier} transmission "
f"(static: {efficiency_static}, per 1000km: {efficiency_per_1000km}, compression per 1000km: {compression_per_1000km}). "
"Splitting bidirectional links."
)
n.links.loc[carrier_i, "p_min_pu"] = 0
n.links.loc[
carrier_i, "efficiency"
] = efficiency_static * efficiency_per_1000km ** (
n.links.loc[carrier_i, "length"] / 1e3
)
rev_links = (
n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1)
)
rev_links["length_original"] = rev_links["length"]
rev_links["capital_cost"] = 0
rev_links["length"] = 0
rev_links["reversed"] = True
rev_links.index = rev_links.index.map(lambda x: x + "-reversed")
n.links = pd.concat([n.links, rev_links], sort=False)
n.links["reversed"] = n.links["reversed"].fillna(False)
n.links["length_original"] = n.links["length_original"].fillna(n.links.length)
# do compression losses after concatenation to take electricity consumption at bus0 in either direction
carrier_i = n.links.query("carrier == @carrier").index
if compression_per_1000km > 0:
n.links.loc[carrier_i, "bus2"] = n.links.loc[carrier_i, "bus0"].map(
n.buses.location
) # electricity
n.links.loc[carrier_i, "efficiency2"] = (
-compression_per_1000km * n.links.loc[carrier_i, "length_original"] / 1e3
)
if __name__ == "__main__": if __name__ == "__main__":
if "snakemake" not in globals(): if "snakemake" not in globals():
from _helpers import mock_snakemake from _helpers import mock_snakemake
@ -3463,7 +3673,7 @@ if __name__ == "__main__":
for carrier in conventional: for carrier in conventional:
add_carrier_buses(n, carrier) add_carrier_buses(n, carrier)
add_co2_tracking(n, options) add_co2_tracking(n, costs, options)
add_generation(n, costs) add_generation(n, costs)
@ -3586,6 +3796,18 @@ if __name__ == "__main__":
if options["electricity_grid_connection"]: if options["electricity_grid_connection"]:
add_electricity_grid_connection(n, costs) add_electricity_grid_connection(n, costs)
for k, v in options["transmission_efficiency"].items():
lossy_bidirectional_links(n, k, v)
# Workaround: Remove lines with conflicting (and unrealistic) properties
# cf. https://github.com/PyPSA/pypsa-eur/issues/444
if snakemake.config["solving"]["options"]["transmission_losses"]:
idx = n.lines.query("num_parallel == 0").index
logger.info(
f"Removing {len(idx)} line(s) with properties conflicting with transmission losses functionality."
)
n.mremove("Line", idx)
first_year_myopic = (snakemake.params.foresight in ["myopic", "perfect"]) and ( first_year_myopic = (snakemake.params.foresight in ["myopic", "perfect"]) and (
snakemake.params.planning_horizons[0] == investment_year snakemake.params.planning_horizons[0] == investment_year
) )

View File

@ -26,8 +26,11 @@ Additionally, some extra constraints specified in :mod:`solve_network` are added
the workflow for all scenarios in the configuration file (``scenario:``) the workflow for all scenarios in the configuration file (``scenario:``)
based on the rule :mod:`solve_network`. based on the rule :mod:`solve_network`.
""" """
import importlib
import logging import logging
import os
import re import re
import sys
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -179,9 +182,6 @@ def add_co2_sequestration_limit(n, config, limit=200):
""" """
Add a global constraint on the amount of Mt CO2 that can be sequestered. 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)
limit = limit * 1e6 limit = limit * 1e6
for o in opts: for o in opts:
if "seq" not in o: if "seq" not in o:
@ -199,10 +199,10 @@ def add_co2_sequestration_limit(n, config, limit=200):
n.madd( n.madd(
"GlobalConstraint", "GlobalConstraint",
names, names,
sense="<=", sense=">=",
constant=limit, constant=-limit,
type="primary_energy", type="operational_limit",
carrier_attribute="co2_absorptions", carrier_attribute="co2 sequestered",
investment_period=periods, investment_period=periods,
) )
@ -393,7 +393,7 @@ def prepare_network(
if snakemake.params["sector"]["limit_max_growth"]["enable"]: if snakemake.params["sector"]["limit_max_growth"]["enable"]:
n = add_max_growth(n, config) n = add_max_growth(n, config)
if n.stores.carrier.eq("co2 stored").any(): if n.stores.carrier.eq("co2 sequestered").any():
limit = co2_sequestration_potential limit = co2_sequestration_potential
add_co2_sequestration_limit(n, config, limit=limit) add_co2_sequestration_limit(n, config, limit=limit)
@ -687,6 +687,35 @@ def add_battery_constraints(n):
n.model.add_constraints(lhs == 0, name="Link-charger_ratio") n.model.add_constraints(lhs == 0, name="Link-charger_ratio")
def add_lossy_bidirectional_link_constraints(n):
if not n.links.p_nom_extendable.any() or not "reversed" in n.links.columns:
return
n.links["reversed"] = n.links.reversed.fillna(0).astype(bool)
carriers = n.links.loc[n.links.reversed, "carrier"].unique()
forward_i = n.links.query(
"carrier in @carriers and ~reversed and p_nom_extendable"
).index
def get_backward_i(forward_i):
return pd.Index(
[
re.sub(r"-(\d{4})$", r"-reversed-\1", s)
if re.search(r"-\d{4}$", s)
else s + "-reversed"
for s in forward_i
]
)
backward_i = get_backward_i(forward_i)
lhs = n.model["Link-p_nom"].loc[backward_i]
rhs = n.model["Link-p_nom"].loc[forward_i]
n.model.add_constraints(lhs == rhs, name="Link-bidirectional_sync")
def add_chp_constraints(n): def add_chp_constraints(n):
electric = ( electric = (
n.links.index.str.contains("urban central") n.links.index.str.contains("urban central")
@ -745,9 +774,13 @@ def add_pipe_retrofit_constraint(n):
""" """
Add constraint for retrofitting existing CH4 pipelines to H2 pipelines. Add constraint for retrofitting existing CH4 pipelines to H2 pipelines.
""" """
gas_pipes_i = n.links.query("carrier == 'gas pipeline' and p_nom_extendable").index if "reversed" not in n.links.columns:
n.links["reversed"] = False
gas_pipes_i = n.links.query(
"carrier == 'gas pipeline' and p_nom_extendable and ~reversed"
).index
h2_retrofitted_i = n.links.query( h2_retrofitted_i = n.links.query(
"carrier == 'H2 pipeline retrofitted' and p_nom_extendable" "carrier == 'H2 pipeline retrofitted' and p_nom_extendable and ~reversed"
).index ).index
if h2_retrofitted_i.empty or gas_pipes_i.empty: if h2_retrofitted_i.empty or gas_pipes_i.empty:
@ -798,12 +831,22 @@ def extra_functionality(n, snapshots):
add_EQ_constraints(n, EQ_o.replace("EQ", "")) add_EQ_constraints(n, EQ_o.replace("EQ", ""))
add_battery_constraints(n) add_battery_constraints(n)
add_lossy_bidirectional_link_constraints(n)
add_pipe_retrofit_constraint(n) add_pipe_retrofit_constraint(n)
if n._multi_invest: if n._multi_invest:
add_carbon_constraint(n, snapshots) add_carbon_constraint(n, snapshots)
add_carbon_budget_constraint(n, snapshots) add_carbon_budget_constraint(n, snapshots)
add_retrofit_gas_boiler_constraint(n, snapshots) add_retrofit_gas_boiler_constraint(n, snapshots)
if snakemake.params.custom_extra_functionality:
source_path = snakemake.params.custom_extra_functionality
assert os.path.exists(source_path), f"{source_path} does not exist"
sys.path.append(os.path.dirname(source_path))
module_name = os.path.splitext(os.path.basename(source_path))[0]
module = importlib.import_module(module_name)
custom_extra_functionality = getattr(module, module_name)
custom_extra_functionality(n, snapshots, snakemake)
def solve_network(n, config, solving, opts="", **kwargs): def solve_network(n, config, solving, opts="", **kwargs):
set_of_options = solving["solver"]["options"] set_of_options = solving["solver"]["options"]
@ -821,6 +864,9 @@ def solve_network(n, config, solving, opts="", **kwargs):
) )
kwargs["assign_all_duals"] = cf_solving.get("assign_all_duals", False) kwargs["assign_all_duals"] = cf_solving.get("assign_all_duals", False)
if kwargs["solver_name"] == "gurobi":
logging.getLogger("gurobipy").setLevel(logging.CRITICAL)
rolling_horizon = cf_solving.pop("rolling_horizon", False) rolling_horizon = cf_solving.pop("rolling_horizon", False)
skip_iterations = cf_solving.pop("skip_iterations", False) skip_iterations = cf_solving.pop("skip_iterations", False)
if not n.lines.s_nom_extendable.any(): if not n.lines.s_nom_extendable.any():
@ -851,6 +897,9 @@ def solve_network(n, config, solving, opts="", **kwargs):
f"Solving status '{status}' with termination condition '{condition}'" f"Solving status '{status}' with termination condition '{condition}'"
) )
if "infeasible" in condition: if "infeasible" in condition:
labels = n.model.compute_infeasibilities()
logger.info("Labels:\n" + labels)
n.model.print_infeasibilities()
raise RuntimeError("Solving status 'infeasible'") raise RuntimeError("Solving status 'infeasible'")
return n return n