update master branch

Merge branch 'master' of https://github.com/PyPSA/pypsa-eur-sec
This commit is contained in:
lisazeyen 2021-02-23 10:16:35 +01:00
commit 8cabeb5871
20 changed files with 867 additions and 310 deletions

View File

@ -26,7 +26,7 @@ the energy system and includes all greenhouse gas emitters except
waste management, agriculture, forestry and land use.
Please see the [documentation](https://pypsa-eur-sec.readthedocs.io/)
for installation instructions and other useful information.
for installation instructions and other useful information about the snakemake workflow.
This diagram gives an overview of the sectors and the links between
them:

View File

@ -8,7 +8,7 @@ wildcard_constraints:
clusters="[0-9]+m?",
sectors="[+a-zA-Z0-9]+",
opts="[-+a-zA-Z0-9]*",
sector_opts="[-+a-zA-Z0-9]*"
sector_opts="[-+a-zA-Z0-9\.\s]*"
@ -292,6 +292,7 @@ rule build_retro_cost:
output:
retro_cost="resources/retro_cost_{network}_s{simpl}_{clusters}.csv",
floor_area="resources/floor_area_{network}_s{simpl}_{clusters}.csv"
resources: mem_mb=1000
script: "scripts/build_retro_cost.py"

View File

@ -1,4 +1,4 @@
version: 0.3.0
version: 0.4.0
logging_level: INFO
@ -15,20 +15,27 @@ scenario:
lv: [1.0,1.5] # allowed transmission line volume expansion, can be any float >= 1.0 (today) or "opt"
clusters: [45,50] # number of nodes in Europe, any integer between 37 (1 node per country-zone) and several hundred
opts: [''] # only relevant for PyPSA-Eur
sector_opts: [Co2L0-3H-T-H-B-I-solar3-dist1] # this is where the main scenario settings are
sector_opts: [Co2L0-3H-T-H-B-I-solar+p3-dist1] # this is where the main scenario settings are
# to really understand the options here, look in scripts/prepare_sector_network.py
# Co2Lx specifies the CO2 target in x% of the 1990 values; default will give default (5%);
# Co2L0p25 will give 25% CO2 emissions; Co2Lm0p05 will give 5% negative emissions
# xH is the temporal resolution; 3H is 3-hourly, i.e. one snapshot every 3 hours
# single letters are sectors: T for land transport, H for building heating,
# B for biomass supply, I for industry, shipping and aviation
# solarx or onwindx changes the available installable potential by factor x
# solar+c0.5 reduces the capital cost of solar to 50\% of reference value
# solar+p3 multiplies the available installable potential by factor 3
# dist{n} includes distribution grids with investment cost of n times cost in data/costs.csv
# for myopic/perfect foresight cb states the carbon budget in GtCO2 (cumulative
# emissions throughout the transition path in the timeframe determined by the
# planning_horizons), be:beta decay; ex:exponential decay
# cb40ex0 distributes a carbon budget of 40 GtCO2 following an exponential
# decay with initial growth rate 0
planning_horizons : [2030] # investment years for myopic and perfect; or costs year for overnight
# for example, set to [2020, 2030, 2040, 2050] for myopic foresight
# CO2 budget as a fraction of 1990 emissions
# this is over-ridden if CO2Lx is set in sector_opts
# this is also over-ridden if cb is set in sector_opts
co2_budget:
2020: 0.7011648746
2025: 0.5241935484
@ -56,11 +63,21 @@ electricity:
battery: 6
H2: 168
# regulate what components with which carriers are kept from PyPSA-Eur;
# some technologies are removed because they are implemented differently
# or have different year-dependent costs in PyPSA-Eur-Sec
pypsa_eur:
"Bus": ["AC"]
"Link": ["DC"]
"Generator": ["onwind", "offwind-ac", "offwind-dc", "solar", "ror"]
"StorageUnit": ["PHS","hydro"]
"Store": []
biomass:
year: 2030
scenario: "Med"
classes:
solid biomass: ['Primary agricultural residues', 'Forestry energy residue', 'Secondary forestry residues', 'Secondary Forestry residues sawdust', 'Forestry residues from landscape care biomass', 'Municipal waste']
solid biomass: ['Primary agricultural residues', 'Forestry energy residue', 'Secondary forestry residues', 'Secondary Forestry residues sawdust', 'Forestry residues from landscape care biomass', 'Municipal waste']
not included: ['Bioethanol sugar beet biomass', 'Rapeseeds for biodiesel', 'sunflower and soya for Biodiesel', 'Starchy crops biomass', 'Grassy crops biomass', 'Willow biomass', 'Poplar biomass potential', 'Roundwood fuelwood', 'Roundwood Chips & Pellets']
biogas: ['Manure biomass potential', 'Sludge biomass']
@ -85,7 +102,7 @@ sector:
'bev_dsm' : True #turns on EV battery
'bev_availability' : 0.5 #How many cars do smart charging
'v2g' : True #allows feed-in to grid from EV battery
#what is not EV or FCEV is fossil-fuelled
#what is not EV or FCEV is oil-fuelled ICE
'land_transport_fuel_cell_share': # 1 means all FCEVs
2020: 0
2030: 0.05
@ -99,7 +116,9 @@ sector:
'transport_fuel_cell_efficiency': 0.5
'transport_internal_combustion_efficiency': 0.3
'shipping_average_efficiency' : 0.4 #For conversion of fuel oil to propulsion in 2011
'time_dep_hp_cop' : True
'time_dep_hp_cop' : True #time dependent heat pump coefficient of performance
'heat_pump_sink_T' : 55. # Celsius, based on DTU / large area radiators; used in build_cop_profiles.py
# conservatively high to cover hot water and space heating in poorly-insulated buildings
'retrofitting' :
'retro_exogen': True # space heat demand savings exogenously
'dE': # reduction of space heat demand (applied before losses in DH)
@ -128,7 +147,9 @@ sector:
'dac' : True
'co2_vent' : True
'SMR' : True
'ccs_fraction' : 0.9
'co2_sequestration_potential' : 200 #MtCO2/a sequestration potential for Europe
'co2_sequestration_cost' : 20 #EUR/tCO2 for transport and sequestration of CO2
'cc_fraction' : 0.9 # default fraction of CO2 captured with post-combustion capture
'hydrogen_underground_storage' : True
'use_fischer_tropsch_waste_heat' : True
'use_fuel_cell_waste_heat' : True
@ -303,7 +324,7 @@ plotting:
"DAC" : "#E74C3C"
"co2 stored" : "#123456"
"CO2 sequestration" : "#123456"
"CCS" : "k"
"CC" : "k"
"co2" : "#123456"
"co2 vent" : "#654321"
"solid biomass for industry co2 from atmosphere" : "#654321"
@ -313,7 +334,7 @@ plotting:
"Fischer-Tropsch" : "#44DD33"
"kerosene for aviation": "#44BB11"
"naphtha for industry" : "#44FF55"
"land transport fossil" : "#44DD33"
"land transport oil" : "#44DD33"
"water tanks" : "#BBBBBB"
"hot water storage" : "#BBBBBB"
"hot water charging" : "#BBBBBB"
@ -348,6 +369,7 @@ plotting:
"process emissions to stored" : "#444444"
"process emissions to atmosphere" : "#888888"
"process emissions" : "#222222"
"oil emissions" : "#666666"
"land transport fuel cell" : "#AAAAAA"
"biogas" : "#800000"
"solid biomass" : "#DAA520"

View File

@ -70,9 +70,9 @@ author = u'2019-2020 Tom Brown (KIT), Marta Victoria (Aarhus University), Lisa Z
# built documents.
#
# The short X.Y version.
version = u'0.3'
version = u'0.4'
# The full version, including alpha/beta/rc tags.
release = u'0.3.0'
release = u'0.4.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -2,7 +2,7 @@ description,file/folder,licence,source
JRC IDEES database,jrc-idees-2015/,CC BY 4.0,https://ec.europa.eu/jrc/en/potencia/jrc-idees
urban/rural fraction,urban_percent.csv,unknown,unknown
JRC biomass potentials,biomass/,unknown,https://doi.org/10.2790/39014
EEA emission statistics,eea/,unknown,https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-14
EEA emission statistics,eea/UNFCCC_v23.csv,EEA standard re-use policy,https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16
Eurostat Energy Balances,eurostat-energy_balances-*/,Eurostat,https://ec.europa.eu/eurostat/web/energy/data/energy-balances
Swiss energy statistics from Swiss Federal Office of Energy,switzerland-sfoe/,unknown,http://www.bfe.admin.ch/themen/00526/00541/00542/02167/index.html?dossier_id=02169
BASt emobility statistics,emobility/,unknown,http://www.bast.de/DE/Verkehrstechnik/Fachthemen/v2-verkehrszaehlung/Stundenwerte.html?nn=626916
@ -17,3 +17,10 @@ IRENA existing VRE capacities,existing_infrastructure/{solar|onwind|offwind}_cap
USGS ammonia production,myb1-2017-nitro.xls,unknown,https://www.usgs.gov/centers/nmic/nitrogen-statistics-and-information
hydrogen salt cavern potentials,hydrogen_salt_cavern_potentials.csv,CC BY 4.0,https://doi.org/10.1016/j.ijhydene.2019.12.161
hotmaps industrial site database,Industrial_Database.csv,CC BY 4.0,https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database
Hotmaps building stock data,data_building_stock.csv,CC BY 4.0,https://gitlab.com/hotmaps/building-stock
U-values Poland,u_values_poland.csv,unknown,https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory
Floor area missing in hotmaps building stock data,floor_area_missing.csv,unknown,https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory
Comparative level investment,comparative_level_investment.csv,Eurostat,https://ec.europa.eu/eurostat/statistics-explained/index.php?title=Comparative_price_levels_for_investment
Electricity taxes,electricity_taxes_eu.csv,Eurostat,https://appsso.eurostat.ec.europa.eu/nui/show.do?dataset=nrg_pc_204&lang=en
Average surface components,average_surface_components.csv,unknown,http://webtool.building-typology.eu/#bm
Retrofitting thermal envelope costs for Germany,retro_cost_germany.csv,unkown,https://www.iwu.de/forschung/handlungslogiken/kosten-energierelevanter-bau-und-anlagenteile-bei-modernisierung/

1 description file/folder licence source
2 JRC IDEES database jrc-idees-2015/ CC BY 4.0 https://ec.europa.eu/jrc/en/potencia/jrc-idees
3 urban/rural fraction urban_percent.csv unknown unknown
4 JRC biomass potentials biomass/ unknown https://doi.org/10.2790/39014
5 EEA emission statistics eea/ eea/UNFCCC_v23.csv unknown EEA standard re-use policy https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-14 https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16
6 Eurostat Energy Balances eurostat-energy_balances-*/ Eurostat https://ec.europa.eu/eurostat/web/energy/data/energy-balances
7 Swiss energy statistics from Swiss Federal Office of Energy switzerland-sfoe/ unknown http://www.bfe.admin.ch/themen/00526/00541/00542/02167/index.html?dossier_id=02169
8 BASt emobility statistics emobility/ unknown http://www.bast.de/DE/Verkehrstechnik/Fachthemen/v2-verkehrszaehlung/Stundenwerte.html?nn=626916
17 USGS ammonia production myb1-2017-nitro.xls unknown https://www.usgs.gov/centers/nmic/nitrogen-statistics-and-information
18 hydrogen salt cavern potentials hydrogen_salt_cavern_potentials.csv CC BY 4.0 https://doi.org/10.1016/j.ijhydene.2019.12.161
19 hotmaps industrial site database Industrial_Database.csv CC BY 4.0 https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database
20 Hotmaps building stock data data_building_stock.csv CC BY 4.0 https://gitlab.com/hotmaps/building-stock
21 U-values Poland u_values_poland.csv unknown https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory
22 Floor area missing in hotmaps building stock data floor_area_missing.csv unknown https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory
23 Comparative level investment comparative_level_investment.csv Eurostat https://ec.europa.eu/eurostat/statistics-explained/index.php?title=Comparative_price_levels_for_investment
24 Electricity taxes electricity_taxes_eu.csv Eurostat https://appsso.eurostat.ec.europa.eu/nui/show.do?dataset=nrg_pc_204&lang=en
25 Average surface components average_surface_components.csv unknown http://webtool.building-typology.eu/#bm
26 Retrofitting thermal envelope costs for Germany retro_cost_germany.csv unkown https://www.iwu.de/forschung/handlungslogiken/kosten-energierelevanter-bau-und-anlagenteile-bei-modernisierung/

View File

@ -66,41 +66,6 @@ PyPSA-Eur-Sec is designed to be imported into the open toolbox `PyPSA <https://w
This project is maintained by the `Energy System Modelling group <https://www.iai.kit.edu/english/2338.php>`_ at the `Institute for Automation and Applied Informatics <https://www.iai.kit.edu/english/index.php>`_ at the `Karlsruhe Institute of Technology <http://www.kit.edu/english/index.php>`_. The group is funded by the `Helmholtz Association <https://www.helmholtz.de/en/>`_ until 2024. Previous versions were developed by the `Renewable Energy Group <https://fias.uni-frankfurt.de/physics/schramm/renewable-energy-system-and-network-analysis/>`_ at `FIAS <https://fias.uni-frankfurt.de/>`_ to carry out simulations for the `CoNDyNet project <http://condynet.de/>`_, financed by the `German Federal Ministry for Education and Research (BMBF) <https://www.bmbf.de/en/index.html>`_ as part of the `Stromnetze Research Initiative <http://forschung-stromnetze.info/projekte/grundlagen-und-konzepte-fuer-effiziente-dezentrale-stromnetze/>`_.
Spatial resolution of sectors
=============================
Not all of the sectors are at the full nodal resolution, and some are
distributed to nodes using heuristics that need to be corrected. Some
networks are copper-plated to reduce computational times.
For example:
Electricity network: nodal.
Electricity demand: nodal, distributed in each country based on
population, GDP and location of industrial facilities.
Building heating demand: nodal, distributed in each country based on
population.
Industry demand: nodal, distributed in each country based on
locations of industry from `HotMaps database <https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database>`_.
Hydrogen network: nodal.
Methane network: single node for Europe, since future demand is so
low and no bottlenecks are expected.
Solid biomass: single node for Europe, until transport costs can be
incorporated.
CO2: single node for Europe, but a transport and storage cost is added for
sequestered CO2.
Liquid hydrocarbons: single node for Europe, since transport costs are low.
Documentation
=============
@ -115,6 +80,20 @@ Documentation
installation
**Implementation details**
* :doc:`spatial_resolution`
* :doc:`supply_demand`
.. toctree::
:hidden:
:maxdepth: 1
:caption: Implementation details
spatial_resolution
supply_demand
**Foresight options**
* :doc:`overnight`

View File

@ -16,7 +16,7 @@ its dependencies. Clone the repository:
.. code:: bash
projects % git clone git@github.com:PyPSA/pypsa-eur.git
projects % git clone https://github.com/PyPSA/pypsa-eur.git
then download and unpack all the PyPSA-Eur data files by running the following snakemake rule:
@ -32,7 +32,7 @@ Next install the technology assumptions database `technology-data <https://githu
.. code:: bash
projects % git clone git@github.com:PyPSA/technology-data.git
projects % git clone https://github.com/PyPSA/technology-data.git
Clone PyPSA-Eur-Sec repository
@ -42,7 +42,7 @@ Create a parallel directory for `PyPSA-Eur-Sec <https://github.com/PyPSA/pypsa-e
.. code:: bash
projects % git clone git@github.com:PyPSA/pypsa-eur-sec.git
projects % git clone https://github.com/PyPSA/pypsa-eur-sec.git
Environment/package requirements
================================
@ -54,6 +54,13 @@ The requirements are the same as `PyPSA-Eur <https://github.com/PyPSA/pypsa-eur>
xarray version >= 0.15.1, you will need the latest master branch of
atlite version 0.0.2.
You can create an enviroment using the environment.yaml file in pypsa-eur/envs:
.../pypsa-eur % conda env create -f envs/environment.yaml
.../pypsa-eur % conda activate pypsa-eur
See details in `PyPSA-Eur Installation <https://pypsa-eur.readthedocs.io/en/latest/installation.html>`_
Data requirements
=================
@ -66,8 +73,8 @@ To download and extract the data bundle on the command line:
.. code:: bash
projects/pypsa-eur-sec/data % wget "https://nworbmot.org/pypsa-eur-sec-data-bundle-201012.tar.gz"
projects/pypsa-eur-sec/data % tar xvzf pypsa-eur-sec-data-bundle-201012.tar.gz
projects/pypsa-eur-sec/data % wget "https://nworbmot.org/pypsa-eur-sec-data-bundle-210125.tar.gz"
projects/pypsa-eur-sec/data % tar xvzf pypsa-eur-sec-data-bundle-210125.tar.gz
The data licences and sources are given in the following table.

View File

@ -6,7 +6,7 @@ Myopic transition path
The myopic code can be used to investigate progressive changes in a network, for instance, those taking place throughout a transition path. The capacities installed in a certain time step are maintained in the network until their operational lifetime expires.
The myopic approach was initially developed and used in the paper `Early decarbonisation of the European Energy system pays off (2020) <https://arxiv.org/abs/2004.11009>`__ but the current implementation complies with the pypsa-eur-sec standard working flow and is compatible with using the higher resolution electricity transmission model `PyPSA-Eur <https://github.com/PyPSA/pypsa-eur>`__ rather than a one-node-per-country model.
The myopic approach was initially developed and used in the paper `Early decarbonisation of the European Energy system pays off (2020) <https://www.nature.com/articles/s41467-020-20015-4>`__ but the current implementation complies with the pypsa-eur-sec standard working flow and is compatible with using the higher resolution electricity transmission model `PyPSA-Eur <https://github.com/PyPSA/pypsa-eur>`__ rather than a one-node-per-country model.
The current code applies the myopic approach to generators, storage technologies and links in the power sector and the space and water heating sector.
@ -61,12 +61,15 @@ Wildcards
The {planning_horizons} wildcard indicates the timesteps in which the network is optimized, e.g. planning_horizons: [2020, 2030, 2040, 2050]
Options
=============
The total carbon budget for the entire transition path can be indicated in the ``scenario.sector_opts`` in ``config.yaml``.
The carbon budget can be split among the ``planning_horizons`` following an exponential or beta decay.
E.g. ``'cb40ex0'`` splits the a carbon budget equal to 40 GtCO_2 following an exponential decay whose initial linear growth rate $r$ is zero
**{co2_budget_name} wildcard**
$e(t) = e_0 (1+ (r+m)t) e^(-mt)$
The {co2_budget_name} wildcard indicates the name of the co2 budget.
A csv file is used as input including the planning_horizons as index, the name of co2_budget as column name, and the maximum co2 emissions (relative to 1990) as values.
See details in Supplementary Note 1 of the paper `Early decarbonisation of the European Energy system pays off (2020) <https://www.nature.com/articles/s41467-020-20015-4>`__
Rules overview
=================
@ -74,17 +77,17 @@ Rules overview
General myopic code structure
===============================
The myopic code solves the network for the time steps included in planning_horizons in a recursive loop, so that:
The myopic code solves the network for the time steps included in ``planning_horizons`` in a recursive loop, so that:
1.The existing capacities (those installed before the base year are added as fixed capacities with p_nom=value, p_nom_extendable=False). E.g. for baseyear=2020, capacities installed before 2020 are added. In addition, the network comprises additional generator, storage, and link capacities with p_nom_extendable=True. The non-solved network is saved in ``results/run_name/networks/prenetworks-brownfield``.
The base year is the first element in planning_horizons. Step 1 is implemented with the rule add_baseyear for the base year and with the rule add_brownfield for the remaining planning_horizons.
The base year is the first element in ``planning_horizons``. Step 1 is implemented with the rule add_baseyear for the base year and with the rule add_brownfield for the remaining planning_horizons.
2.The 2020 network is optimized. The solved network is saved in results/run_name/networks/postnetworks
2.The 2020 network is optimized. The solved network is saved in ``results/run_name/networks/postnetworks``
3.For the next planning horizon, e.g. 2030, the capacities from a previous time step are added if they are still in operation (i.e., if they fulfil planning horizon <= commissioned year + lifetime). In addition, the network comprises additional generator, storage, and link capacities with p_nom_extendable=True. The non-solved network is saved in ``results/run_name/networks/prenetworks-brownfield``.
Steps 2 and 3 are solved recursively for all the planning_horizons included in the configuration file.
Steps 2 and 3 are solved recursively for all the planning_horizons included in ``config.yaml``.
rule add_existing baseyear
@ -110,8 +113,8 @@ Then, the resulting network is saved in ``results/run_name/networks/prenetworks-
rule add_brownfield
===================
The rule add_brownfield loads the network in results/run_name/networks/prenetworks and performs the following operation:
The rule add_brownfield loads the network in ``results/run_name/networks/prenetworks`` and performs the following operation:
1.Read the capacities optimized in the previous time step and add them to the network if they are still in operation (i.e., if they fulfil planning horizon < commissioned year + lifetime)
1.Read the capacities optimized in the previous time step and add them to the network if they are still in operation (i.e., if they fulfill planning horizon < commissioned year + lifetime)
Then, the resulting network is saved in ``results/run_name/networks/prenetworks_brownfield``.

View File

@ -2,6 +2,38 @@
Release Notes
##########################################
Future release
===================
* For the myopic investment option, a carbon budget and a type of decay (exponential or beta) can be selected in the ``config.yaml`` file to distribute the budget across the ``planning_horizons``. For example, ``cb40ex0`` in the ``{sector_opts}`` wildcard will distribute a carbon budget of 40 GtCO2 following an exponential decay with initial growth rate 0.
* Added an option to alter the capital cost or maximum capacity of carriers by a factor via ``carrier+factor`` in the ``{sector_opts}`` wildcard. This can be useful for exploring uncertain cost parameters. Example: ``solar+c0.5`` reduces the ``capital_cost`` of solar to 50\% of original values. Similarly ``solar+p3`` multiplies the ``p_nom_max`` by 3.
* Rename the bus for European liquid hydrocarbons from ``Fischer-Tropsch`` to ``EU oil``, since it can be supplied not just with the Fischer-Tropsch process, but also with fossil oil.
* Bugfix: Fix reading in of ``pypsa-eur/resources/powerplants.csv`` to PyPSA-Eur Version 0.3.0 (use column attribute name ``DateIn`` instead of old ``YearDecommissioned``).
* Bugfix: Make sure that ``Store`` components (battery and H2) are also removed from PyPSA-Eur, so they can be added later by PyPSA-Eur-Sec.
PyPSA-Eur-Sec 0.4.0 (11th December 2020)
=========================================
This release includes a more accurate nodal disaggregation of industry demand within each country, fixes to CHP and CCS representations, as well as changes to some configuration settings.
It has been released to coincide with `PyPSA-Eur <https://github.com/PyPSA/pypsa-eur>`_ Version 0.3.0 and `Technology Data <https://github.com/PyPSA/technology-data>`_ Version 0.2.0, and is known to work with these releases.
New features:
* The `Hotmaps Industrial Database <https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database>`_ is used to disaggregate the industrial demand spatially to the nodes inside each country (previously it was distributed by population density).
* Electricity demand from industry is now separated from the regular electricity demand and distributed according to the industry demand. Only the remaining regular electricity demand for households and services is distributed according to GDP and population.
* A cost database for the retrofitting of the thermal envelope of residential and services buildings has been integrated, as well as endogenous optimisation of the level of retrofitting. This is described in the paper `Mitigating heat demand peaks in buildings in a highly renewable European energy system <https://arxiv.org/abs/2012.01831>`_. Retrofitting can be activated both exogenously and endogenously from the ``config.yaml``.
* The biomass and gas combined heat and power (CHP) parameters ``c_v`` and ``c_b`` were read in assuming they were extraction plants rather than back pressure plants. The data is now corrected in `Technology Data <https://github.com/PyPSA/technology-data>`_ Version 0.2.0 to the correct DEA back pressure assumptions and they are now implemented as single links with a fixed ratio of electricity to heat output (even as extraction plants, they were always sitting on the backpressure line in simulations, so there was no point in modelling the full heat-electricity feasibility polygon). The old assumptions underestimated the heat output.
* The Danish Energy Agency released `new assumptions for carbon capture <https://ens.dk/en/our-services/projections-and-models/technology-data/technology-data-industrial-process-heat-and>`_ in October 2020, which have now been incorporated in PyPSA-Eur-Sec, including direct air capture (DAC) and post-combustion capture on CHPs, cement kilns and other industrial facilities. The electricity and heat demand for DAC is modelled for each node (with heat coming from district heating), but currently the electricity and heat demand for industrial capture is not modelled very cleanly (for process heat, 10% of the energy is assumed to go to carbon capture) - a new issue will be opened on this.
* Land transport is separated by energy carrier (fossil, hydrogen fuel cell electric vehicle, and electric vehicle), but still needs to be separated into heavy and light vehicles (the data is there, just not the code yet).
* For assumptions that change with the investment year, there is a new time-dependent format in the ``config.yaml`` using a dictionary with keys for each year. Implemented examples include the CO2 budget, exogenous retrofitting share and land transport energy carrier; more parameters will be dynamised like this in future.
* Some assumptions have been moved out of the code and into the ``config.yaml``, including the carbon sequestration potential and cost, the heat pump sink temperature, reductions in demand for high value chemicals, and some BEV DSM parameters and transport efficiencies.
* Documentation on :doc:`supply_demand` options has been added.
Many thanks to Fraunhofer ISI for opening the hotmaps database and to Lisa Zeyen (KIT) for implementing the building retrofitting.
PyPSA-Eur-Sec 0.3.0 (27th September 2020)
=========================================
@ -92,6 +124,8 @@ Release Process
* Update version number in ``doc/conf.py`` and ``*config.*.yaml``.
* Make a ``git commit``.
* Tag a release by running ``git tag v0.x.x``, ``git push``, ``git push --tags``. Include release notes in the tag message.
* Make a `GitHub release <https://github.com/PyPSA/pypsa-eur-sec/releases>`_, which automatically triggers archiving by `zenodo <https://doi.org/10.5281/zenodo.3938042>`_.
@ -102,4 +136,4 @@ To make a new release of the data bundle, make an archive of the files in ``data
.. code:: bash
data % tar pczf pypsa-eur-sec-data-bundle-date.tar.gz eea switzerland-sfoe biomass eurostat-energy_balances-* jrc-idees-2015 emobility urban_percent.csv timezone_mappings.csv heat_load_profile_DK_AdamJensen.csv WindWaveWEC_GLTB.xlsx myb1-2017-nitro.xls Industrial_Database.csv
data % tar pczf pypsa-eur-sec-data-bundle-YYMMDD.tar.gz eea/UNFCCC_v23.csv switzerland-sfoe biomass eurostat-energy_balances-* jrc-idees-2015 emobility urban_percent.csv timezone_mappings.csv heat_load_profile_DK_AdamJensen.csv WindWaveWEC_GLTB.xlsx myb1-2017-nitro.xls Industrial_Database.csv

View File

@ -0,0 +1,54 @@
.. _spatial_resolution:
##########################################
Spatial resolution
##########################################
The default nodal resolution of the model follows the electricity
generation and transmission model `PyPSA-Eur
<https://github.com/PyPSA/pypsa-eur>`_, which clusters down the
electricity transmission substations in each European country based on
the k-means algorithm. This gives nodes which correspond to major load
and generation centres (typically cities).
The total number of nodes for Europe is set in the ``config.yaml`` file
under ``clusters``. The number of nodes can vary between 37, the number
of independent countries / synchronous areas, and several
hundred. With 200-300 nodes the model needs 100-150 GB RAM to solve
with a commerical solver like Gurobi.
Not all of the sectors are at the full nodal resolution, and some
demand for some sectors is distributed to nodes using heuristics that
need to be corrected. Some networks are copper-plated to reduce
computational times.
For example:
Electricity network: nodal.
Electricity residential and commercial demand: nodal, distributed in
each country based on population and GDP.
Electricity demand in industry: based on the location of industrial
facilities from `HotMaps database <https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database>`_.
Building heating demand: nodal, distributed in each country based on
population.
Industry demand: nodal, distributed in each country based on
locations of industry from `HotMaps database <https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database>`_.
Hydrogen network: nodal.
Methane network: single node for Europe, since future demand is so
low and no bottlenecks are expected.
Solid biomass: single node for Europe, until transport costs can be
incorporated.
CO2: single node for Europe, but a transport and storage cost is added for
sequestered CO2.
Liquid hydrocarbons: single node for Europe, since transport costs for
liquids are low.

193
doc/supply_demand.rst Normal file
View File

@ -0,0 +1,193 @@
.. _supply_demand:
##########################################
Supply and demand
##########################################
An initial orientation to the supply and demand options in the model
PyPSA-Eur-Sec can be found in the description of the model
PyPSA-Eur-Sec-30 in the paper `Synergies of sector coupling and
transmission reinforcement in a cost-optimised, highly renewable
European energy system <https://arxiv.org/abs/1801.05290>`_ (2018).
The latest version of PyPSA-Eur-Sec differs by including biomass,
industry, industrial feedstocks, aviation, shipping, better carbon
management, carbon capture and usage/sequestration, and gas networks.
The basic supply (left column) and demand (right column) options in the model are described in this figure:
.. image:: ../graphics/multisector_figure.png
Electricity supply and demand
=============================
Electricity supply and demand follows the electricity generation and
transmission model `PyPSA-Eur <https://github.com/PyPSA/pypsa-eur>`_,
except that hydrogen storage is integrated into the hydrogen supply,
demand and network, and PyPSA-Eur-Sec includes CHPs.
Unlike PyPSA-Eur, PyPSA-Eur-Sec does not distribution electricity demand for industry according to population and GDP, but uses the
geographical data from the `Hotmaps Industrial Database
<https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database>`_.
Also unlike PyPSA-Eur, PyPSA-Eur-Sec subtracts existing electrified heating from the existing electricity demand, so that power-to-heat can be optimised separately.
The remaining electricity demand for households and services is distributed inside each country proportional to GDP and population.
Heat demand
=============================
Heat demand is split into:
* ``urban central``: large-scale district heating networks in urban areas with dense heat demand
* ``residential/services urban decentral``: heating for individual buildings in urban areas
* ``residential/services rural``: heating for individual buildings in rural areas
Heat supply
=======================
Oil and gas boilers
--------------------
Heat pumps
-------------
Either air-to-water or ground-to-water heat pumps are implemented.
They have coefficient of performance (COP) based on either the
external air or the soil hourly temperature.
Ground-source heat pumps are only allowed in rural areas because of
space constraints.
Only air-source heat pumps are allowed in urban areas. This is a
conservative assumption, since there are many possible sources of
low-temperature heat that could be tapped in cities (waste water,
rivers, lakes, seas, etc.).
Resistive heaters
--------------------
Large Combined Heat and Power (CHP) plants
--------------------------------------------
A good summary of CHP options that can be implemented in PyPSA can be found in the paper `Cost sensitivity of optimal sector-coupled district heating production systems <https://doi.org/10.1016/j.energy.2018.10.044>`_.
PyPSA-Eur-Sec includes CHP plants fuelled by methane, hydrogen and solid biomass from waste and residues.
Hydrogen CHPs are fuel cells.
Methane and biomass CHPs are based on back pressure plants operating with a fixed ratio of electricity to heat output. The methane CHP is modelled on the Danish Energy Agency (DEA) "Gas turbine simple cycle (large)" while the solid biomass CHP is based on the DEA's "09b Wood Pellets Medium".
The efficiencies of each are given on the back pressure line, where the back pressure coefficient ``c_b`` is the electricity output divided by the heat output. The plants are not allowed to deviate from the back pressure line and are implement as ``Link`` objects with a fixed ratio of heat to electricity output.
NB: The old PyPSA-Eur-Sec-30 model assumed an extraction plant (like the DEA coal CHP) for gas which has flexible production of heat and electricity within the feasibility diagram of Figure 4 in the `Synergies paper <https://arxiv.org/abs/1801.05290>`_. We have switched to the DEA back pressure plants since these are more common for smaller plants for biomass, and because the extraction plants were on the back pressure line for 99.5% of the time anyway. The plants were all changed to back pressure in PyPSA-Eur-Sec v0.4.0.
Micro-CHP for individual buildings
-----------------------------------
Optional.
Waste heat from Fuel Cells, Methanation and Fischer-Tropsch plants
-------------------------------------------------------------------
Solar thermal collectors
-------------------------
Thermal energy storage using hot water tanks
---------------------------------------------
Small for decentral applications.
Big water pit storage for district heating.
Hydrogen demand
==================
Stationary fuel cell CHP.
Transport applications.
Industry (ammonia, precursor to hydrocarbons for chemicals and iron/steel).
Hydrogen supply
=================
Steam Methane Reforming (SMR), SMR+CCS, electrolysers.
Methane demand
==================
Can be used in boilers, in CHPs, in industry for high temperature heat, in OCGT.
Not used in transport because of engine slippage.
Methane supply
=================
Fossil, biogas, Sabatier (hydrogen to methane), HELMETH (directly power to methane with efficient heat integration).
Solid biomass demand
=====================
Solid biomass provides process heat up to 500 Celsius in industry, as well as feeding CHP plants in district heating networks.
Solid biomass supply
=====================
Only wastes and residues from the JRC biomass dataset.
Oil product demand
=====================
Transport fuels and naphtha as a feedstock for the chemicals industry.
Oil product supply
======================
Fossil or Fischer-Tropsch.
Industry demand
================
Based on materials demand from JRC-IDEES and other sources such as the USGS for ammonia.
Industry is split into many sectors, including iron and steel, ammonia, other basic chemicals, cement, non-metalic minerals, alumuninium, other non-ferrous metals, pulp, paper and printing, food, beverages and tobacco, and other more minor sectors.
Inside each country the industrial demand is distributed using the `Hotmaps Industrial Database <https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database>`_.
Industry supply
================
Process switching (e.g. from blast furnaces to direct reduction and electric arc furnaces for steel) is defined exogenously.
Fuel switching for process heat is mostly also done exogenously.
Solid biomass is used for up to 500 Celsius, mostly in paper and pulp and food and beverages.
Higher temperatures are met with methane.
Carbon dioxide capture, usage and sequestration (CCU/S)
=========================================================
Carbon dioxide can be captured from industry process emissions,
emissions related to industry process heat, combined heat and power
plants, and directly from the air (DAC).
Carbon dioxide can be used as an input for methanation and
Fischer-Tropsch fuels, or it can be sequestered underground.

View File

@ -125,7 +125,7 @@ def add_existing_renewables(df_agg):
if capacity > 0.:
df_agg.at[name,"Fueltype"] = tech
df_agg.at[name,"Capacity"] = capacity
df_agg.at[name,"YearCommissioned"] = year
df_agg.at[name,"DateIn"] = year
df_agg.at[name,"cluster_bus"] = node
def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, baseyear):
@ -182,7 +182,7 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas
add_existing_renewables(df_agg)
df_agg["grouping_year"] = np.take(grouping_years,
np.digitize(df_agg.YearCommissioned,
np.digitize(df_agg.DateIn,
grouping_years,
right=True))
@ -249,7 +249,7 @@ def add_heating_capacities_installed_before_baseyear(n, baseyear, grouping_years
grouping_years : intervals to group existing capacities
linear decomissioning of heating capacities from 2020 to 2045 is
linear decommissioning of heating capacities from 2020 to 2045 is
currently assumed
heating capacities split between residential and services proportional
@ -408,18 +408,18 @@ if __name__ == "__main__":
if 'snakemake' not in globals():
from vresutils.snakemake import MockSnakemake
snakemake = MockSnakemake(
wildcards=dict(network='elec', simpl='', clusters='39', lv='1.0',
sector_opts='Co2L0-168H-T-H-B-I-solar3-dist1',
co2_budget_name='b30b3',
wildcards=dict(network='elec', simpl='', clusters='45', lv='1.0',
sector_opts='Co2L0-3H-T-H-B-I-solar3-dist1',
planning_horizons='2020'),
input=dict(network='pypsa-eur-sec/results/test/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{co2_budget_name}_{planning_horizons}.nc',
input=dict(network='pypsa-eur-sec/results/version-2/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc',
powerplants='pypsa-eur/resources/powerplants.csv',
busmap_s='pypsa-eur/resources/busmap_{network}_s{simpl}.csv',
busmap='pypsa-eur/resources/busmap_{network}_s{simpl}_{clusters}.csv',
costs='pypsa-eur-sec/data/costs/costs_{planning_horizons}.csv',
costs='technology_data/outputs/costs_{planning_horizons}.csv',
cop_air_total="pypsa-eur-sec/resources/cop_air_total_{network}_s{simpl}_{clusters}.nc",
cop_soil_total="pypsa-eur-sec/resources/cop_soil_total_{network}_s{simpl}_{clusters}.nc"),
output=['pypsa-eur-sec/results/test/prenetworks_brownfield/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc'],
cop_soil_total="pypsa-eur-sec/resources/cop_soil_total_{network}_s{simpl}_{clusters}.nc",
clustered_pop_layout="pypsa-eur-sec/resources/pop_layout_{network}_s{simpl}_{clusters}.csv",),
output=['pypsa-eur-sec/results/version-2/prenetworks_brownfield/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc'],
)
import yaml
with open('config.yaml', encoding='utf8') as f:

View File

@ -1,3 +1,4 @@
# coding: utf-8
import pandas as pd
@ -57,7 +58,15 @@ if __name__ == "__main__":
snakemake.input['jrc_potentials'] = "data/biomass/JRC Biomass Potentials.xlsx"
snakemake.output = Dict()
snakemake.output['biomass_potentials'] = 'data/biomass_potentials.csv'
snakemake.output['biomass_potentials_all']='resources/biomass_potentials_all.csv'
with open('config.yaml', encoding='utf8') as f:
snakemake.config = yaml.safe_load(f)
# This is a hack, to be replaced once snakemake is unicode-conform
if 'Secondary Forestry residues sawdust' in snakemake.config['biomass']['classes']['solid biomass']:
snakemake.config['biomass']['classes']['solid biomass'].remove('Secondary Forestry residues sawdust')
snakemake.config['biomass']['classes']['solid biomass'].append('Secondary Forestry residues sawdust')
build_biomass_potentials()

View File

@ -9,16 +9,13 @@ import xarray as xr
cop_f = {"air" : lambda d_t: 6.81 -0.121*d_t + 0.000630*d_t**2,
"soil" : lambda d_t: 8.77 -0.150*d_t + 0.000734*d_t**2}
sink_T = 55. # Based on DTU / large area radiators
for area in ["total", "urban", "rural"]:
for source in ["air", "soil"]:
source_T = xr.open_dataarray(snakemake.input["temp_{}_{}".format(source,area)])
delta_T = sink_T - source_T
delta_T = snakemake.config['sector']['heat_pump_sink_T'] - source_T
cop = cop_f[source](delta_T)

View File

@ -390,12 +390,12 @@ def build_energy_totals():
return clean_df
def build_eea_co2():
def build_eea_co2(year=1990):
# see ../notebooks/compute_1990_Europe_emissions_for_targets.ipynb
#https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-14
#downloaded 190222 (modified by EEA last on 181130)
fn = "data/eea/UNFCCC_v21.csv"
#https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16
#downloaded 201228 (modified by EEA last on 201221)
fn = "data/eea/UNFCCC_v23.csv"
df = pd.read_csv(fn, encoding="latin-1")
df.loc[df["Year"] == "1985-1987","Year"] = 1986
df["Year"] = df["Year"].astype(int)
@ -418,16 +418,14 @@ def build_eea_co2():
e['waste management'] = '5 - Waste management'
e['other'] = '6 - Other Sector'
e['indirect'] = 'ind_CO2 - Indirect CO2'
e["total wL"] = "Total (with LULUCF, with indirect CO2)"
e["total woL"] = "Total (without LULUCF, with indirect CO2)"
e["total wL"] = "Total (with LULUCF)"
e["total woL"] = "Total (without LULUCF)"
pol = "CO2" #["All greenhouse gases - (CO2 equivalent)","CO2"]
cts = ["CH","EUA","NO"] + eu28_eea
year = 1990
emissions = df.loc[idx[cts,pol,year,e.values],"emissions"].unstack("Sector_name").rename(columns=pd.Series(e.index,e.values)).rename(index={"All greenhouse gases - (CO2 equivalent)" : "GHG"},level=1)
#only take level 0, since level 1 (pol) and level 2 (year) are trivial
@ -467,7 +465,7 @@ def build_eurostat_co2(year=1990):
return eurostat_co2
def build_co2_totals(year=1990):
def build_co2_totals(eea_co2, eurostat_co2, year=1990):
co2 = eea_co2.reindex(["EU28","NO","CH","BA","RS","AL","ME","MK"] + eu28)
@ -486,10 +484,6 @@ def build_co2_totals(year=1990):
#doesn't include non-energy emissions
co2.loc[ct,'agriculture'] = eurostat_co2[ct,"+","+","Agriculture / Forestry"].sum()
co2.to_csv(snakemake.output.co2_name)
return co2
@ -547,7 +541,7 @@ if __name__ == "__main__":
snakemake.output['transport_name'] = "data/transport_data.csv"
snakemake.input = Dict()
snakemake.input['nuts3_shapes'] = 'resources/nuts3_shapes.geojson'
snakemake.input['nuts3_shapes'] = '../pypsa-eur/resources/nuts3_shapes.geojson'
nuts3 = gpd.read_file(snakemake.input.nuts3_shapes).set_index('index')
population = nuts3['pop'].groupby(nuts3.country).sum()
@ -566,6 +560,7 @@ if __name__ == "__main__":
eurostat_co2 = build_eurostat_co2()
build_co2_totals()
co2=build_co2_totals(eea_co2, eurostat_co2, year)
co2.to_csv(snakemake.output.co2_name)
build_transport_data()

View File

@ -23,7 +23,6 @@ Structure:
import pandas as pd
import matplotlib.pyplot as plt
pd.options.mode.chained_assignment = None
#%% ************ FUCNTIONS ***************************************************
@ -175,9 +174,9 @@ def prepare_building_stock_data():
area = building_data[(building_data.type == 'Heated area [Mm²]') &
(building_data.subsector != "Total")]
area_tot = area.groupby(["country", "sector"]).sum()
area["weight"] = area.apply(lambda x: x.value /
area = pd.concat([area, area.apply(lambda x: x.value /
area_tot.value.loc[(x.country, x.sector)],
axis=1)
axis=1).rename("weight")],axis=1)
area = area.groupby(['country', 'sector', 'subsector', 'bage']).sum()
area_tot.rename(index=country_iso_dic, inplace=True)
@ -192,9 +191,9 @@ def prepare_building_stock_data():
pop_layout["ct"] = pop_layout.index.str[:2]
ct_total = pop_layout.total.groupby(pop_layout["ct"]).sum()
area_per_pop = area_tot.unstack().apply(lambda x: x / ct_total[x.index])
area_per_pop = area_tot.unstack().reindex(index=ct_total.index).apply(lambda x: x / ct_total[x.index])
missing_area_ct = ct_total.index.difference(area_tot.index.levels[0])
for ct in missing_area_ct:
for ct in (missing_area_ct & ct_total.index):
averaged_data = pd.DataFrame(
area_per_pop.value.reindex(map_for_missings[ct]).mean()
* ct_total[ct],
@ -233,7 +232,7 @@ def prepare_building_stock_data():
# smallest possible today u values for windows 0.8 (passive house standard)
# maybe the u values for the glass and not the whole window including frame
# for those types assumed in the dataset
u_values[(u_values.type=="Windows") & (u_values.value<0.8)]["value"] = 0.8
u_values.loc[(u_values.type=="Windows") & (u_values.value<0.8), "value"] = 0.8
# drop unnecessary columns
u_values.drop(['topic', 'feature','detail', 'estimated','unit'],
axis=1, inplace=True, errors="ignore")
@ -314,8 +313,12 @@ def calculate_cost_energy_curve(u_values, l_strength, l_weight, average_surface_
for l in l_strength:
u_values[l] = calculate_new_u(u_values, l, l_weight)
energy_saved[l] = calculate_dE(u_values, l, average_surface_w)
costs[l] = calculate_costs(u_values, l, cost_retro, average_surface)
energy_saved = pd.concat([energy_saved,
calculate_dE(u_values, l, average_surface_w).rename(l)],
axis=1)
costs = pd.concat([costs,
calculate_costs(u_values, l, cost_retro, average_surface).rename(l)],
axis=1)
# energy and costs per country, sector, subsector and year
e_tot = energy_saved.groupby(['country', 'sector', 'subsector', 'bage']).sum()
@ -334,11 +337,13 @@ def calculate_cost_energy_curve(u_values, l_strength, l_weight, average_surface_
axis=1, keys=["dE", "cost"])
res.rename(index=country_iso_dic, inplace=True)
res = res.loc[countries]
res = res.reindex(index=countries, level=0)
# reset index because otherwise not considered countries still in index.levels[0]
res = res.reset_index().set_index(["country", "sector"])
# map missing countries
for ct in map_for_missings.keys():
averaged_data = pd.DataFrame(res.loc[map_for_missings[ct], :].mean(level=1))
for ct in pd.Index(map_for_missings.keys()) & countries:
averaged_data = res.reindex(index=map_for_missings[ct], level=0).mean(level=1)
index = pd.MultiIndex.from_product([[ct], averaged_data.index.to_list()])
averaged_data.index = index
if ct not in res.index.levels[0]:
@ -436,12 +441,14 @@ if __name__ == "__main__":
# for missing weighting of surfaces of building types assume MultiFamily houses
u_values["assumed_subsector"] = u_values.subsector
u_values.assumed_subsector[
~u_values.subsector.isin(average_surface.index)] = 'Multifamily houses'
u_values.loc[~u_values.subsector.isin(average_surface.index),
"assumed_subsector"] = 'Multifamily houses'
dE_and_cost = calculate_cost_energy_curve(u_values, l_strength, l_weight,
average_surface_w, average_surface, area,
country_iso_dic, countries)
# reset index because otherwise not considered countries still in index.levels[0]
dE_and_cost = dE_and_cost.reset_index().set_index(["country", "sector"])
# weights costs after construction index
if construction_index:

View File

@ -28,10 +28,13 @@ opt_name = {"Store": "e", "Line" : "s", "Transformer" : "s"}
override_component_attrs = pypsa.descriptors.Dict({k : v.copy() for k,v in pypsa.components.component_attrs.items()})
override_component_attrs["Link"].loc["bus2"] = ["string",np.nan,np.nan,"2nd bus","Input (optional)"]
override_component_attrs["Link"].loc["bus3"] = ["string",np.nan,np.nan,"3rd bus","Input (optional)"]
override_component_attrs["Link"].loc["bus4"] = ["string",np.nan,np.nan,"4th bus","Input (optional)"]
override_component_attrs["Link"].loc["efficiency2"] = ["static or series","per unit",1.,"2nd bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["efficiency3"] = ["static or series","per unit",1.,"3rd bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["efficiency4"] = ["static or series","per unit",1.,"4th bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["p2"] = ["series","MW",0.,"2nd bus output","Output"]
override_component_attrs["Link"].loc["p3"] = ["series","MW",0.,"3rd bus output","Output"]
override_component_attrs["Link"].loc["p4"] = ["series","MW",0.,"4th bus output","Output"]
override_component_attrs["StorageUnit"].loc["p_dispatch"] = ["series","MW",0.,"Storage discharging.","Output"]
override_component_attrs["StorageUnit"].loc["p_store"] = ["series","MW",0.,"Storage charging.","Output"]
@ -193,7 +196,25 @@ def calculate_costs(n,label,costs):
return costs
def calculate_cumulative_cost():
planning_horizons = snakemake.config['scenario']['planning_horizons']
cumulative_cost = pd.DataFrame(index = df["costs"].sum().index,
columns=pd.Series(data=np.arange(0,0.1, 0.01), name='social discount rate'))
#discount cost and express them in money value of planning_horizons[0]
for r in cumulative_cost.columns:
cumulative_cost[r]=[df["costs"].sum()[index]/((1+r)**(index[-1]-planning_horizons[0])) for index in cumulative_cost.index]
#integrate cost throughout the transition path
for r in cumulative_cost.columns:
for cluster in cumulative_cost.index.get_level_values(level=0).unique():
for lv in cumulative_cost.index.get_level_values(level=1).unique():
for sector_opts in cumulative_cost.index.get_level_values(level=2).unique():
cumulative_cost.loc[(cluster, lv, sector_opts,'cumulative cost'),r] = np.trapz(cumulative_cost.loc[idx[cluster, lv, sector_opts,planning_horizons],r].values, x=planning_horizons)
return cumulative_cost
def calculate_nodal_capacities(n,label,nodal_capacities):
#Beware this also has extraneous locations for country (e.g. biomass) or continent-wide (e.g. fossil gas/oil) stuff
for c in n.iterate_components(n.branch_components|n.controllable_one_port_components^{"Load"}):
@ -561,16 +582,17 @@ if __name__ == "__main__":
snakemake.config = yaml.safe_load(f)
#overwrite some options
snakemake.config["run"] = "test"
snakemake.config["run"] = "version-8"
snakemake.config["scenario"]["lv"] = [1.0]
snakemake.config["scenario"]["sector_opts"] = ["Co2L0-168H-T-H-B-I-solar3-dist1"]
snakemake.config["scenario"]["sector_opts"] = ["3H-T-H-B-I-solar3-dist1"]
snakemake.config["planning_horizons"] = ['2020', '2030', '2040', '2050']
snakemake.input = Dict()
snakemake.input['heat_demand_name'] = 'data/heating/daily_heat_demand.h5'
snakemake.input['costs'] = snakemake.config['costs_dir'] + "costs_{}.csv".format(snakemake.config['scenario']['planning_horizons'][0])
snakemake.output = Dict()
for item in outputs:
snakemake.output[item] = snakemake.config['summary_dir'] + '/{name}/csvs/{item}.csv'.format(name=snakemake.config['run'],item=item)
snakemake.output['cumulative_cost'] = snakemake.config['summary_dir'] + '/{name}/csvs/cumulative_cost.csv'.format(name=snakemake.config['run'])
networks_dict = {(cluster, lv, opt+sector_opt, planning_horizon) :
snakemake.config['results_dir'] + snakemake.config['run'] + '/postnetworks/elec_s{simpl}_{cluster}_lv{lv}_{opt}_{sector_opt}_{planning_horizon}.nc'\
.format(simpl=simpl,
@ -589,6 +611,7 @@ if __name__ == "__main__":
print(networks_dict)
Nyears = 1
costs_db = prepare_costs(snakemake.input.costs,
snakemake.config['costs']['USD2013_to_EUR2013'],
snakemake.config['costs']['discountrate'],
@ -600,3 +623,10 @@ if __name__ == "__main__":
df["metrics"].loc["total costs"] = df["costs"].sum()
to_csv(df)
if snakemake.config["foresight"]=='myopic':
cumulative_cost=calculate_cumulative_cost()
cumulative_cost.to_csv(snakemake.config['summary_dir'] + '/' + snakemake.config['run'] + '/csvs/cumulative_cost.csv')

View File

@ -1,6 +1,6 @@
import numpy as np
import pandas as pd
#allow plotting without Xwindows
@ -9,7 +9,7 @@ matplotlib.use('Agg')
import matplotlib.pyplot as plt
from prepare_sector_network import co2_emissions_year
#consolidate and rename
def rename_techs(label):
@ -22,7 +22,7 @@ def rename_techs(label):
"retrofitting" : "building retrofitting",
"H2" : "hydrogen storage",
"battery" : "battery storage",
"CCS" : "CCS"}
"CC" : "CC"}
rename = {"solar" : "solar PV",
"Sabatier" : "methanation",
@ -237,7 +237,137 @@ def plot_balances():
fig.savefig(snakemake.output.balances[:-10] + k + ".pdf",transparent=True)
def historical_emissions(cts):
"""
read historical emissions to add them to the carbon budget plot
"""
#https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16
#downloaded 201228 (modified by EEA last on 201221)
fn = "data/eea/UNFCCC_v23.csv"
df = pd.read_csv(fn, encoding="latin-1")
df.loc[df["Year"] == "1985-1987","Year"] = 1986
df["Year"] = df["Year"].astype(int)
df = df.set_index(['Year', 'Sector_name', 'Country_code', 'Pollutant_name']).sort_index()
e = pd.Series()
e["electricity"] = '1.A.1.a - Public Electricity and Heat Production'
e['residential non-elec'] = '1.A.4.b - Residential'
e['services non-elec'] = '1.A.4.a - Commercial/Institutional'
e['rail non-elec'] = "1.A.3.c - Railways"
e["road non-elec"] = '1.A.3.b - Road Transportation'
e["domestic navigation"] = "1.A.3.d - Domestic Navigation"
e['international navigation'] = '1.D.1.b - International Navigation'
e["domestic aviation"] = '1.A.3.a - Domestic Aviation'
e["international aviation"] = '1.D.1.a - International Aviation'
e['total energy'] = '1 - Energy'
e['industrial processes'] = '2 - Industrial Processes and Product Use'
e['agriculture'] = '3 - Agriculture'
e['LULUCF'] = '4 - Land Use, Land-Use Change and Forestry'
e['waste management'] = '5 - Waste management'
e['other'] = '6 - Other Sector'
e['indirect'] = 'ind_CO2 - Indirect CO2'
e["total wL"] = "Total (with LULUCF)"
e["total woL"] = "Total (without LULUCF)"
pol = ["CO2"] # ["All greenhouse gases - (CO2 equivalent)"]
cts
if "GB" in cts:
cts.remove("GB")
cts.append("UK")
year = np.arange(1990,2018).tolist()
idx = pd.IndexSlice
co2_totals = df.loc[idx[year,e.values,cts,pol],"emissions"].unstack("Year").rename(index=pd.Series(e.index,e.values))
co2_totals = (1/1e6)*co2_totals.groupby(level=0, axis=0).sum() #Gton CO2
co2_totals.loc['industrial non-elec'] = co2_totals.loc['total energy'] - co2_totals.loc[['electricity', 'services non-elec','residential non-elec', 'road non-elec',
'rail non-elec', 'domestic aviation', 'international aviation', 'domestic navigation',
'international navigation']].sum()
emissions = co2_totals.loc["electricity"]
if "T" in opts:
emissions += co2_totals.loc[[i+ " non-elec" for i in ["rail","road"]]].sum()
if "H" in opts:
emissions += co2_totals.loc[[i+ " non-elec" for i in ["residential","services"]]].sum()
if "I" in opts:
emissions += co2_totals.loc[["industrial non-elec","industrial processes",
"domestic aviation","international aviation",
"domestic navigation","international navigation"]].sum()
return emissions
def plot_carbon_budget_distribution():
"""
Plot historical carbon emissions in the EU and decarbonization path
"""
import matplotlib.gridspec as gridspec
import seaborn as sns; sns.set()
sns.set_style('ticks')
plt.style.use('seaborn-ticks')
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['ytick.direction'] = 'in'
plt.rcParams['xtick.labelsize'] = 20
plt.rcParams['ytick.labelsize'] = 20
plt.figure(figsize=(10, 7))
gs1 = gridspec.GridSpec(1, 1)
ax1 = plt.subplot(gs1[0,0])
ax1.set_ylabel('CO$_2$ emissions (Gt per year)',fontsize=22)
ax1.set_ylim([0,5])
ax1.set_xlim([1990,snakemake.config['scenario']['planning_horizons'][-1]+1])
path_cb = snakemake.config['results_dir'] + snakemake.config['run'] + '/csvs/'
countries=pd.read_csv(path_cb + 'countries.csv', index_col=1)
cts=countries.index.to_list()
e_1990 = co2_emissions_year(cts, opts, year=1990)
CO2_CAP=pd.read_csv(path_cb + 'carbon_budget_distribution.csv',
index_col=0)
ax1.plot(e_1990*CO2_CAP[o],linewidth=3,
color='dodgerblue', label=None)
emissions = historical_emissions(cts)
ax1.plot(emissions, color='black', linewidth=3, label=None)
#plot commited and uder-discussion targets
#(notice that historical emissions include all countries in the
# network, but targets refer to EU)
ax1.plot([2020],[0.8*emissions[1990]],
marker='*', markersize=12, markerfacecolor='black',
markeredgecolor='black')
ax1.plot([2030],[0.45*emissions[1990]],
marker='*', markersize=12, markerfacecolor='white',
markeredgecolor='black')
ax1.plot([2030],[0.6*emissions[1990]],
marker='*', markersize=12, markerfacecolor='black',
markeredgecolor='black')
ax1.plot([2050, 2050],[x*emissions[1990] for x in [0.2, 0.05]],
color='gray', linewidth=2, marker='_', alpha=0.5)
ax1.plot([2050],[0.01*emissions[1990]],
marker='*', markersize=12, markerfacecolor='white',
linewidth=0, markeredgecolor='black',
label='EU under-discussion target', zorder=10,
clip_on=False)
ax1.plot([2050],[0.125*emissions[1990]],'ro',
marker='*', markersize=12, markerfacecolor='black',
markeredgecolor='black', label='EU commited target')
ax1.legend(fancybox=True, fontsize=18, loc=(0.01,0.01),
facecolor='white', frameon=True)
path_cb_plot = snakemake.config['results_dir'] + snakemake.config['run'] + '/graphs/'
plt.savefig(path_cb_plot+'carbon_budget_plot.pdf', dpi=300)
if __name__ == "__main__":
# Detect running outside of snakemake and mock snakemake for testing
@ -249,13 +379,16 @@ if __name__ == "__main__":
snakemake.config = yaml.safe_load(f)
snakemake.input = Dict()
snakemake.output = Dict()
snakemake.wildcards = Dict()
#snakemake.wildcards['sector_opts']='3H-T-H-B-I-solar3-dist1-cb48be3'
for item in ["costs", "energy"]:
snakemake.input[item] = snakemake.config['summary_dir'] + '/{name}/csvs/{item}.csv'.format(name=snakemake.config['run'],item=item)
snakemake.output[item] = snakemake.config['summary_dir'] + '/{name}/graphs/{item}.pdf'.format(name=snakemake.config['run'],item=item)
snakemake.input["balances"] = snakemake.config['summary_dir'] + '/test/csvs/supply_energy.csv'
snakemake.output["balances"] = snakemake.config['summary_dir'] + '/test/graphs/balances-energy.csv'
snakemake.input["balances"] = snakemake.config['summary_dir'] + '/{name}/csvs/supply_energy.csv'.format(name=snakemake.config['run'],item=item)
snakemake.output["balances"] = snakemake.config['summary_dir'] + '/{name}/graphs/balances-energy.csv'.format(name=snakemake.config['run'],item=item)
n_header = 4
plot_costs()
@ -263,3 +396,9 @@ if __name__ == "__main__":
plot_energy()
plot_balances()
for sector_opts in snakemake.config['scenario']['sector_opts']:
opts=sector_opts.split('-')
for o in opts:
if "cb" in o:
plot_carbon_budget_distribution()

View File

@ -20,6 +20,8 @@ import pytz
from vresutils.costdata import annuity
from scipy.stats import beta
from build_energy_totals import build_eea_co2, build_eurostat_co2, build_co2_totals
#First tell PyPSA that links can have multiple outputs by
#overriding the component_attrs. This can be done for
@ -30,10 +32,13 @@ from vresutils.costdata import annuity
override_component_attrs = pypsa.descriptors.Dict({k : v.copy() for k,v in pypsa.components.component_attrs.items()})
override_component_attrs["Link"].loc["bus2"] = ["string",np.nan,np.nan,"2nd bus","Input (optional)"]
override_component_attrs["Link"].loc["bus3"] = ["string",np.nan,np.nan,"3rd bus","Input (optional)"]
override_component_attrs["Link"].loc["bus4"] = ["string",np.nan,np.nan,"4th bus","Input (optional)"]
override_component_attrs["Link"].loc["efficiency2"] = ["static or series","per unit",1.,"2nd bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["efficiency3"] = ["static or series","per unit",1.,"3rd bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["efficiency4"] = ["static or series","per unit",1.,"4th bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["p2"] = ["series","MW",0.,"2nd bus output","Output"]
override_component_attrs["Link"].loc["p3"] = ["series","MW",0.,"3rd bus output","Output"]
override_component_attrs["Link"].loc["p4"] = ["series","MW",0.,"4th bus output","Output"]
override_component_attrs["Link"].loc["build_year"] = ["integer","year",np.nan,"build year","Input (optional)"]
override_component_attrs["Link"].loc["lifetime"] = ["float","years",np.nan,"lifetime","Input (optional)"]
@ -42,6 +47,90 @@ override_component_attrs["Generator"].loc["lifetime"] = ["float","years",np.nan,
override_component_attrs["Store"].loc["build_year"] = ["integer","year",np.nan,"build year","Input (optional)"]
override_component_attrs["Store"].loc["lifetime"] = ["float","years",np.nan,"lifetime","Input (optional)"]
def co2_emissions_year(cts, opts, year):
"""
calculate co2 emissions in one specific year (e.g. 1990 or 2018).
"""
eea_co2 = build_eea_co2(year)
#TODO: read Eurostat data from year>2014, this only affects the estimation of
# CO2 emissions for "BA","RS","AL","ME","MK"
if year > 2014:
eurostat_co2 = build_eurostat_co2(year=2014)
else:
eurostat_co2 = build_eurostat_co2(year)
co2_totals=build_co2_totals(eea_co2, eurostat_co2, year)
co2_emissions = co2_totals.loc[cts, "electricity"].sum()
if "T" in opts:
co2_emissions += co2_totals.loc[cts, [i+ " non-elec" for i in ["rail","road"]]].sum().sum()
if "H" in opts:
co2_emissions += co2_totals.loc[cts, [i+ " non-elec" for i in ["residential","services"]]].sum().sum()
if "I" in opts:
co2_emissions += co2_totals.loc[cts, ["industrial non-elec","industrial processes",
"domestic aviation","international aviation",
"domestic navigation","international navigation"]].sum().sum()
co2_emissions *=0.001 #MtCO2 to GtCO2
return co2_emissions
def build_carbon_budget(o):
#distribute carbon budget following beta or exponential transition path
if "be" in o:
#beta decay
carbon_budget = float(o[o.find("cb")+2:o.find("be")])
be=float(o[o.find("be")+2:])
if "ex" in o:
#exponential decay
carbon_budget = float(o[o.find("cb")+2:o.find("ex")])
r=float(o[o.find("ex")+2:])
pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout, index_col=0)
pop_layout["ct"] = pop_layout.index.str[:2]
cts = pop_layout.ct.value_counts().index
e_1990 = co2_emissions_year(cts, opts, year=1990)
#emissions at the beginning of the path (last year available 2018)
e_0 = co2_emissions_year(cts, opts, year=2018)
#emissions in 2019 and 2020 assumed equal to 2018 and substracted
carbon_budget -= 2*e_0
planning_horizons = snakemake.config['scenario']['planning_horizons']
CO2_CAP = pd.DataFrame(index = pd.Series(data=planning_horizons,
name='planning_horizon'),
columns=pd.Series(data=[],
name='paths',
dtype='float'))
t_0 = planning_horizons[0]
if "be" in o:
#beta decay
t_f = t_0 + (2*carbon_budget/e_0).round(0) # final year in the path
#emissions (relative to 1990)
CO2_CAP[o] = [(e_0/e_1990)*(1-beta.cdf((t-t_0)/(t_f-t_0), be, be)) for t in planning_horizons]
if "ex" in o:
#exponential decay without delay
T=carbon_budget/e_0
m=(1+np.sqrt(1+r*T))/T
CO2_CAP[o] = [(e_0/e_1990)*(1+(m+r)*(t-t_0))*np.exp(-m*(t-t_0)) for t in planning_horizons]
CO2_CAP.to_csv(path_cb + 'carbon_budget_distribution.csv', sep=',',
line_terminator='\n', float_format='%.3f')
countries=pd.Series(data=cts)
countries.to_csv(path_cb + 'countries.csv', sep=',',
line_terminator='\n', float_format='%.3f')
def add_lifetime_wind_solar(n):
"""
Add lifetime for solar and wind generators
@ -152,16 +241,15 @@ def remove_elec_base_techs(n):
"""remove conventional generators (e.g. OCGT) and storage units (e.g. batteries and H2)
from base electricity-only network, since they're added here differently using links
"""
to_keep = {"generators" : snakemake.config["plotting"]["vre_techs"],
"storage_units" : snakemake.config["plotting"]["renewable_storage_techs"]}
n.carriers = n.carriers.loc[to_keep["generators"] + to_keep["storage_units"]]
for components, techs in iteritems(to_keep):
df = getattr(n,components)
to_remove = df.carrier.value_counts().index^techs
print("removing {} with carrier {}".format(components,to_remove))
df.drop(df.index[df.carrier.isin(to_remove)],inplace=True)
for c in n.iterate_components(snakemake.config["pypsa_eur"]):
to_keep = snakemake.config["pypsa_eur"][c.name]
to_remove = pd.Index(c.df.carrier.unique())^to_keep
print("Removing",c.list_name,"with carrier",to_remove)
names = c.df.index[c.df.carrier.isin(to_remove)]
print(names)
n.mremove(c.name, names)
n.carriers.drop(to_remove, inplace=True, errors="ignore")
def remove_non_electric_buses(n):
@ -199,12 +287,10 @@ def add_co2_tracking(n):
location="EU",
carrier="co2 stored")
#TODO move cost to data/costs.csv
#TODO move maximum somewhere more transparent
n.madd("Store",["co2 stored"],
e_nom_extendable = True,
e_nom_max=2e8,
capital_cost=20.,
e_nom_extendable=True,
e_nom_max=options['co2_sequestration_potential']*1e6,
capital_cost=options['co2_sequestration_cost'],
carrier="co2 stored",
bus="co2 stored")
@ -216,17 +302,26 @@ def add_co2_tracking(n):
efficiency=1.,
p_nom_extendable=True)
if options['dac']:
#direct air capture consumes electricity to take CO2 from the air to the underground store
#TODO do with cost from Breyer - later use elec and heat and capital cost
n.madd("Link",["DAC"],
bus0="co2 atmosphere",
bus1="co2 stored",
carrier="DAC",
marginal_cost=75.,
efficiency=1.,
p_nom_extendable=True,
lifetime=costs.at['DAC','lifetime'])
def add_dac(n):
heat_buses = n.buses.index[n.buses.carrier.isin(["urban central heat",
"services urban decentral heat"])]
locations = n.buses.location[heat_buses]
n.madd("Link",
locations,
suffix=" DAC",
bus0="co2 atmosphere",
bus1="co2 stored",
bus2=locations.values,
bus3=heat_buses,
carrier="DAC",
capital_cost=costs.at['direct air capture','fixed'],
efficiency=1.,
efficiency2=-(costs.at['direct air capture','electricity-input'] + costs.at['direct air capture','compression-electricity-input']),
efficiency3=-(costs.at['direct air capture','heat-input'] - costs.at['direct air capture','compression-heat-output']),
p_nom_extendable=True,
lifetime=costs.at['direct air capture','lifetime'])
def add_co2limit(n, Nyears=1.,limit=0.):
@ -596,7 +691,7 @@ def add_generation(network):
capital_cost=0.) #could correct to e.g. 0.2 EUR/kWh * annuity and O&M
network.add("Generator",
"EU fossil " + carrier,
"EU " + carrier,
bus="EU " + carrier,
p_nom_extendable=True,
carrier=carrier,
@ -944,18 +1039,18 @@ def add_storage(network):
if options['SMR']:
network.madd("Link",
nodes + " SMR CCS",
nodes + " SMR CC",
bus0=["EU gas"]*len(nodes),
bus1=nodes+" H2",
bus2="co2 atmosphere",
bus3="co2 stored",
p_nom_extendable=True,
carrier="SMR CCS",
efficiency=costs.at["SMR CCS","efficiency"],
efficiency2=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]),
efficiency3=costs.at['gas','CO2 intensity']*options["ccs_fraction"],
capital_cost=costs.at["SMR CCS","fixed"],
lifetime=costs.at['SMR CCS','lifetime'])
carrier="SMR CC",
efficiency=costs.at["SMR CC","efficiency"],
efficiency2=costs.at['gas','CO2 intensity']*(1-options["cc_fraction"]),
efficiency3=costs.at['gas','CO2 intensity']*options["cc_fraction"],
capital_cost=costs.at["SMR CC","fixed"],
lifetime=costs.at['SMR CC','lifetime'])
network.madd("Link",
nodes + " SMR",
@ -976,14 +1071,14 @@ def add_land_transport(network):
fuel_cell_share = get_parameter(options["land_transport_fuel_cell_share"])
electric_share = get_parameter(options["land_transport_electric_share"])
fossil_share = 1 - fuel_cell_share - electric_share
ice_share = 1 - fuel_cell_share - electric_share
print("shares of FCEV, EV and ICEV are",
fuel_cell_share,
electric_share,
fossil_share)
ice_share)
if fossil_share < 0:
if ice_share < 0:
print("Error, more FCEV and EV share than 1.")
sys.exit()
@ -1061,14 +1156,14 @@ def add_land_transport(network):
p_set=fuel_cell_share/options['transport_fuel_cell_efficiency']*transport[nodes])
if fossil_share > 0:
if ice_share > 0:
network.madd("Load",
nodes,
suffix=" land transport fossil",
bus="Fischer-Tropsch",
carrier="land transport fossil",
p_set=fossil_share/options['transport_internal_combustion_efficiency']*transport[nodes])
suffix=" land transport oil",
bus="EU oil",
carrier="land transport oil",
p_set=ice_share/options['transport_internal_combustion_efficiency']*transport[nodes])
@ -1234,64 +1329,36 @@ def add_heat(network):
if name == "urban central":
#add gas CHP; biomass CHP is added in biomass section
network.madd("Link",
nodes[name] + " urban central gas CHP electric",
nodes[name] + " urban central gas CHP",
bus0="EU gas",
bus1=nodes[name],
bus2="co2 atmosphere",
carrier="urban central gas CHP electric",
bus2=nodes[name] + " urban central heat",
bus3="co2 atmosphere",
carrier="urban central gas CHP",
p_nom_extendable=True,
capital_cost=costs.at['central gas CHP','fixed']*costs.at['central gas CHP','efficiency'],
marginal_cost=costs.at['central gas CHP','VOM'],
efficiency=costs.at['central gas CHP','efficiency'],
efficiency2=costs.at['gas','CO2 intensity'],
c_b=costs.at['central gas CHP','c_b'],
c_v=costs.at['central gas CHP','c_v'],
p_nom_ratio=costs.at['central gas CHP','p_nom_ratio'],
efficiency2=costs.at['central gas CHP','efficiency']/costs.at['central gas CHP','c_b'],
efficiency3=costs.at['gas','CO2 intensity'],
lifetime=costs.at['central gas CHP','lifetime'])
network.madd("Link",
nodes[name] + " urban central gas CHP heat",
bus0="EU gas",
bus1=nodes[name] + " urban central heat",
bus2="co2 atmosphere",
carrier="urban central gas CHP heat",
p_nom_extendable=True,
marginal_cost=costs.at['central gas CHP','VOM'],
efficiency=costs.at['central gas CHP','efficiency']/costs.at['central gas CHP','c_v'],
efficiency2=costs.at['gas','CO2 intensity'],
lifetime=costs.at['central gas CHP','lifetime'])
network.madd("Link",
nodes[name] + " urban central gas CHP CCS electric",
nodes[name] + " urban central gas CHP CC",
bus0="EU gas",
bus1=nodes[name],
bus2="co2 atmosphere",
bus3="co2 stored",
carrier="urban central gas CHP CCS electric",
bus2=nodes[name] + " urban central heat",
bus3="co2 atmosphere",
bus4="co2 stored",
carrier="urban central gas CHP CC",
p_nom_extendable=True,
capital_cost=costs.at['central gas CHP CCS','fixed']*costs.at['central gas CHP CCS','efficiency'],
marginal_cost=costs.at['central gas CHP CCS','VOM'],
efficiency=costs.at['central gas CHP CCS','efficiency'],
efficiency2=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]),
efficiency3=costs.at['gas','CO2 intensity']*options["ccs_fraction"],
c_b=costs.at['central gas CHP CCS','c_b'],
c_v=costs.at['central gas CHP CCS','c_v'],
p_nom_ratio=costs.at['central gas CHP CCS','p_nom_ratio'],
lifetime=costs.at['central gas CHP CCS','lifetime'])
network.madd("Link",
nodes[name] + " urban central gas CHP CCS heat",
bus0="EU gas",
bus1=nodes[name] + " urban central heat",
bus2="co2 atmosphere",
bus3="co2 stored",
carrier="urban central gas CHP CCS heat",
p_nom_extendable=True,
marginal_cost=costs.at['central gas CHP CCS','VOM'],
efficiency=costs.at['central gas CHP CCS','efficiency']/costs.at['central gas CHP CCS','c_v'],
efficiency2=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]),
efficiency3=costs.at['gas','CO2 intensity']*options["ccs_fraction"],
lifetime=costs.at['central gas CHP CCS','lifetime'])
capital_cost=costs.at['central gas CHP','fixed']*costs.at['central gas CHP','efficiency'] + costs.at['biomass CHP capture','fixed']*costs.at['gas','CO2 intensity'],
marginal_cost=costs.at['central gas CHP','VOM'],
efficiency=costs.at['central gas CHP','efficiency'] - costs.at['gas','CO2 intensity']*(costs.at['biomass CHP capture','electricity-input'] + costs.at['biomass CHP capture','compression-electricity-input']),
efficiency2=costs.at['central gas CHP','efficiency']/costs.at['central gas CHP','c_b'] + costs.at['gas','CO2 intensity']*(costs.at['biomass CHP capture','heat-output'] + costs.at['biomass CHP capture','compression-heat-output'] - costs.at['biomass CHP capture','heat-input']),
efficiency3=costs.at['gas','CO2 intensity']*(1-costs.at['biomass CHP capture','capture_rate']),
efficiency4=costs.at['gas','CO2 intensity']*costs.at['biomass CHP capture','capture_rate'],
lifetime=costs.at['central gas CHP','lifetime'])
else:
if options["micro_chp"]:
@ -1483,61 +1550,35 @@ def add_biomass(network):
urban_central = urban_central.str[:-len(" urban central heat")]
network.madd("Link",
urban_central + " urban central solid biomass CHP electric",
urban_central + " urban central solid biomass CHP",
bus0="EU solid biomass",
bus1=urban_central,
carrier="urban central solid biomass CHP electric",
bus2=urban_central + " urban central heat",
carrier="urban central solid biomass CHP",
p_nom_extendable=True,
capital_cost=costs.at['central solid biomass CHP','fixed']*costs.at['central solid biomass CHP','efficiency'],
marginal_cost=costs.at['central solid biomass CHP','VOM'],
efficiency=costs.at['central solid biomass CHP','efficiency'],
c_b=costs.at['central solid biomass CHP','c_b'],
c_v=costs.at['central solid biomass CHP','c_v'],
p_nom_ratio=costs.at['central solid biomass CHP','p_nom_ratio'],
lifetime=costs.at['central solid biomass CHP','lifetime'])
network.madd("Link",
urban_central + " urban central solid biomass CHP heat",
bus0="EU solid biomass",
bus1=urban_central + " urban central heat",
carrier="urban central solid biomass CHP heat",
p_nom_extendable=True,
marginal_cost=costs.at['central solid biomass CHP','VOM'],
efficiency=costs.at['central solid biomass CHP','efficiency']/costs.at['central solid biomass CHP','c_v'],
efficiency2=costs.at['central solid biomass CHP','efficiency-heat'],
lifetime=costs.at['central solid biomass CHP','lifetime'])
network.madd("Link",
urban_central + " urban central solid biomass CHP CCS electric",
urban_central + " urban central solid biomass CHP CC",
bus0="EU solid biomass",
bus1=urban_central,
bus2="co2 atmosphere",
bus3="co2 stored",
carrier="urban central solid biomass CHP CCS electric",
bus2=urban_central + " urban central heat",
bus3="co2 atmosphere",
bus4="co2 stored",
carrier="urban central solid biomass CHP CC",
p_nom_extendable=True,
capital_cost=costs.at['central solid biomass CHP CCS','fixed']*costs.at['central solid biomass CHP CCS','efficiency'],
marginal_cost=costs.at['central solid biomass CHP CCS','VOM'],
efficiency=costs.at['central solid biomass CHP CCS','efficiency'],
efficiency2=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"],
efficiency3=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"],
c_b=costs.at['central solid biomass CHP','c_b'],
c_v=costs.at['central solid biomass CHP','c_v'],
p_nom_ratio=costs.at['central solid biomass CHP','p_nom_ratio'],
lifetime=costs.at['central solid biomass CHP CCS','lifetime'])
capital_cost=costs.at['central solid biomass CHP','fixed']*costs.at['central solid biomass CHP','efficiency'] + costs.at['biomass CHP capture','fixed']*costs.at['solid biomass','CO2 intensity'],
marginal_cost=costs.at['central solid biomass CHP','VOM'],
efficiency=costs.at['central solid biomass CHP','efficiency'] - costs.at['solid biomass','CO2 intensity']*(costs.at['biomass CHP capture','electricity-input'] + costs.at['biomass CHP capture','compression-electricity-input']),
efficiency2=costs.at['central solid biomass CHP','efficiency-heat'] + costs.at['solid biomass','CO2 intensity']*(costs.at['biomass CHP capture','heat-output'] + costs.at['biomass CHP capture','compression-heat-output'] - costs.at['biomass CHP capture','heat-input']),
efficiency3=-costs.at['solid biomass','CO2 intensity']*costs.at['biomass CHP capture','capture_rate'],
efficiency4=costs.at['solid biomass','CO2 intensity']*costs.at['biomass CHP capture','capture_rate'],
lifetime=costs.at['central solid biomass CHP','lifetime'])
network.madd("Link",
urban_central + " urban central solid biomass CHP CCS heat",
bus0="EU solid biomass",
bus1=urban_central + " urban central heat",
bus2="co2 atmosphere",
bus3="co2 stored",
carrier="urban central solid biomass CHP CCS heat",
p_nom_extendable=True,
marginal_cost=costs.at['central solid biomass CHP CCS','VOM'],
efficiency=costs.at['central solid biomass CHP CCS','efficiency']/costs.at['central solid biomass CHP CCS','c_v'],
efficiency2=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"],
efficiency3=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"],
lifetime=costs.at['central solid biomass CHP CCS','lifetime'])
def add_industry(network):
@ -1573,18 +1614,18 @@ def add_industry(network):
efficiency=1.)
network.madd("Link",
["solid biomass for industry CCS"],
["solid biomass for industry CC"],
bus0="EU solid biomass",
bus1="solid biomass for industry",
bus2="co2 atmosphere",
bus3="co2 stored",
carrier="solid biomass for industry CCS",
carrier="solid biomass for industry CC",
p_nom_extendable=True,
capital_cost=costs.at["industry CCS","fixed"]*costs.at['solid biomass','CO2 intensity']*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h)
capital_cost=costs.at["cement capture","fixed"]*costs.at['solid biomass','CO2 intensity'],
efficiency=0.9,
efficiency2=-costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"],
efficiency3=costs.at['solid biomass','CO2 intensity']*options["ccs_fraction"],
lifetime=costs.at['industry CCS','lifetime'])
efficiency2=-costs.at['solid biomass','CO2 intensity']*costs.at["cement capture","capture_rate"],
efficiency3=costs.at['solid biomass','CO2 intensity']*costs.at["cement capture","capture_rate"],
lifetime=costs.at['cement capture','lifetime'])
network.madd("Bus",
@ -1609,18 +1650,18 @@ def add_industry(network):
efficiency2=costs.at['gas','CO2 intensity'])
network.madd("Link",
["gas for industry CCS"],
["gas for industry CC"],
bus0="EU gas",
bus1="gas for industry",
bus2="co2 atmosphere",
bus3="co2 stored",
carrier="gas for industry CCS",
carrier="gas for industry CC",
p_nom_extendable=True,
capital_cost=costs.at["industry CCS","fixed"]*costs.at['gas','CO2 intensity']*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h)
capital_cost=costs.at["cement capture","fixed"]*costs.at['gas','CO2 intensity'],
efficiency=0.9,
efficiency2=costs.at['gas','CO2 intensity']*(1-options["ccs_fraction"]),
efficiency3=costs.at['gas','CO2 intensity']*options["ccs_fraction"],
lifetime=costs.at['industry CCS','lifetime'])
efficiency2=costs.at['gas','CO2 intensity']*(1-costs.at["cement capture","capture_rate"]),
efficiency3=costs.at['gas','CO2 intensity']**costs.at["cement capture","capture_rate"],
lifetime=costs.at['cement capture','lifetime'])
network.madd("Load",
@ -1638,27 +1679,30 @@ def add_industry(network):
carrier="H2 for shipping",
p_set = nodal_energy_totals.loc[nodes,["total international navigation","total domestic navigation"]].sum(axis=1)*1e6*options['shipping_average_efficiency']/costs.at["fuel cell","efficiency"]/8760.)
network.madd("Bus",
["Fischer-Tropsch"],
location="EU",
carrier="Fischer-Tropsch")
if "EU oil" not in network.buses.index:
network.madd("Bus",
["EU oil"],
location="EU",
carrier="oil")
#use madd to get carrier inserted
network.madd("Store",
["Fischer-Tropsch Store"],
bus="Fischer-Tropsch",
e_nom_extendable=True,
e_cyclic=True,
carrier="Fischer-Tropsch",
capital_cost=0.) #could correct to e.g. 0.001 EUR/kWh * annuity and O&M
if "EU oil Store" not in network.stores.index:
network.madd("Store",
["EU oil Store"],
bus="EU oil",
e_nom_extendable=True,
e_cyclic=True,
carrier="oil",
capital_cost=0.) #could correct to e.g. 0.001 EUR/kWh * annuity and O&M
network.add("Generator",
"fossil oil",
bus="Fischer-Tropsch",
p_nom_extendable=True,
carrier="oil",
capital_cost=0.,
marginal_cost=costs.at["oil",'fuel'])
if "EU oil" not in network.generators.index:
network.add("Generator",
"EU oil",
bus="EU oil",
p_nom_extendable=True,
carrier="oil",
capital_cost=0.,
marginal_cost=costs.at["oil",'fuel'])
if options["oil_boilers"]:
@ -1668,7 +1712,7 @@ def add_industry(network):
network.madd("Link",
nodes_heat[name] + " " + name + " oil boiler",
p_nom_extendable=True,
bus0=["Fischer-Tropsch"] * len(nodes_heat[name]),
bus0="EU oil",
bus1=nodes_heat[name] + " " + name + " heat",
bus2="co2 atmosphere",
carrier=name + " oil boiler",
@ -1681,7 +1725,7 @@ def add_industry(network):
network.madd("Link",
nodes + " Fischer-Tropsch",
bus0=nodes + " H2",
bus1="Fischer-Tropsch",
bus1="EU oil",
bus2="co2 stored",
carrier="Fischer-Tropsch",
efficiency=costs.at["Fischer-Tropsch",'efficiency'],
@ -1692,13 +1736,13 @@ def add_industry(network):
network.madd("Load",
["naphtha for industry"],
bus="Fischer-Tropsch",
bus="EU oil",
carrier="naphtha for industry",
p_set = industrial_demand.loc[nodes,"naphtha"].sum()/8760.)
network.madd("Load",
["kerosene for aviation"],
bus="Fischer-Tropsch",
bus="EU oil",
carrier="kerosene for aviation",
p_set = nodal_energy_totals.loc[nodes,["total international aviation","total domestic aviation"]].sum(axis=1).sum()*1e6/8760.)
@ -1708,9 +1752,9 @@ def add_industry(network):
co2 = network.loads.loc[["naphtha for industry","kerosene for aviation"],"p_set"].sum()*costs.at["oil",'CO2 intensity'] - industrial_demand.loc[nodes,"process emission from feedstock"].sum()/8760.
network.madd("Load",
["Fischer-Tropsch emissions"],
["oil emissions"],
bus="co2 atmosphere",
carrier="Fischer-Tropsch emissions",
carrier="oil emissions",
p_set=-co2)
network.madd("Load",
@ -1754,18 +1798,18 @@ def add_industry(network):
p_nom_extendable=True,
efficiency=1.)
#assume enough local waste heat for CCS
#assume enough local waste heat for CC
network.madd("Link",
["process emissions CCS"],
["process emissions CC"],
bus0="process emissions",
bus1="co2 atmosphere",
bus2="co2 stored",
carrier="process emissions CCS",
carrier="process emissions CC",
p_nom_extendable=True,
capital_cost=costs.at["industry CCS","fixed"]*8760, #8760 converts EUR/(tCO2/a) to EUR/(tCO2/h)
efficiency=(1-options["ccs_fraction"]),
efficiency2=options["ccs_fraction"],
lifetime=costs.at['industry CCS','lifetime'])
capital_cost=costs.at["cement capture","fixed"],
efficiency=(1-costs.at["cement capture","capture_rate"]),
efficiency2=costs.at["cement capture","capture_rate"],
lifetime=costs.at['cement capture','lifetime'])
@ -1786,13 +1830,6 @@ def add_waste_heat(network):
network.links.loc[urban_central + " H2 Fuel Cell","bus2"] = urban_central + " urban central heat"
network.links.loc[urban_central + " H2 Fuel Cell","efficiency2"] = 0.95 - network.links.loc[urban_central + " H2 Fuel Cell","efficiency"]
def restrict_technology_potential(n,tech,limit):
print("restricting potentials (p_nom_max) for {} to {} of technical potential".format(tech,limit))
gens = n.generators.index[n.generators.carrier.str.contains(tech)]
#beware if limit is 0 and p_nom_max is np.inf, 0*np.inf is nan
n.generators.loc[gens,"p_nom_max"] *=limit
def decentral(n):
n.lines.drop(n.lines.index,inplace=True)
n.links.drop(n.links.index[n.links.carrier.isin(["DC","B2B"])],inplace=True)
@ -1828,15 +1865,16 @@ def get_parameter(item):
return item
#%%
if __name__ == "__main__":
# Detect running outside of snakemake and mock snakemake for testing
if 'snakemake' not in globals():
from vresutils.snakemake import MockSnakemake
snakemake = MockSnakemake(
wildcards=dict(network='elec', simpl='', clusters='37', lv='1.0',
opts='', planning_horizons='2030', co2_budget_name="go",
sector_opts='Co2L0-120H-T-H-B-I-solar3-dist1'),
opts='', planning_horizons='2020',
sector_opts='120H-T-H-B-I-onwind+p3-dist1-cb48be3'),
input=dict( network='../pypsa-eur/networks/{network}_s{simpl}_{clusters}_ec_lv{lv}_{opts}.nc',
energy_totals_name='resources/energy_totals.csv',
co2_totals_name='resources/co2_totals.csv',
@ -1872,10 +1910,11 @@ if __name__ == "__main__":
solar_thermal_total="resources/solar_thermal_total_{network}_s{simpl}_{clusters}.nc",
solar_thermal_urban="resources/solar_thermal_urban_{network}_s{simpl}_{clusters}.nc",
solar_thermal_rural="resources/solar_thermal_rural_{network}_s{simpl}_{clusters}.nc",
retro_cost_energy = "resources/retro_cost_{network}_s{simpl}_{clusters}.csv",
retro_cost_energy = "resources/retro_cost_{network}_s{simpl}_{clusters}.csv",
floor_area = "resources/floor_area_{network}_s{simpl}_{clusters}.csv"
),
output=['pypsa-eur-sec/results/test/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{co2_budget_name}_{planning_horizons}.nc']
output=['results/version-cb48be3/prenetworks/{network}_s{simpl}_{clusters}_lv{lv}__{sector_opts}_{planning_horizons}.nc']
)
import yaml
with open('config.yaml', encoding='utf8') as f:
@ -1960,6 +1999,9 @@ if __name__ == "__main__":
if "I" in opts and "H" in opts:
add_waste_heat(n)
if options['dac']:
add_dac(n)
if "decentral" in opts:
decentral(n)
@ -1978,6 +2020,22 @@ if __name__ == "__main__":
limit = get_parameter(snakemake.config["co2_budget"])
print("CO2 limit set to",limit)
for o in opts:
if "cb" in o:
path_cb = snakemake.config['results_dir'] + snakemake.config['run'] + '/csvs/'
if not os.path.exists(path_cb):
os.makedirs(path_cb)
try:
CO2_CAP=pd.read_csv(path_cb + 'carbon_budget_distribution.csv', index_col=0)
except:
build_carbon_budget(o)
CO2_CAP=pd.read_csv(path_cb + 'carbon_budget_distribution.csv', index_col=0)
limit=CO2_CAP.loc[investment_year]
print("overriding CO2 limit with scenario limit",limit)
for o in opts:
if "Co2L" in o:
limit = o[o.find("Co2L")+4:]
@ -1987,14 +2045,7 @@ if __name__ == "__main__":
print("adding CO2 budget limit as per unit of 1990 levels of",limit)
add_co2limit(n, Nyears, limit)
for o in opts:
for tech in ["solar","onwind","offwind"]:
if tech in o:
limit = o[o.find(tech)+len(tech):]
limit = float(limit.replace("p",".").replace("m","-"))
print("changing potential for",tech,"by factor",limit)
restrict_technology_potential(n,tech,limit)
if o[:10] == 'linemaxext':
maxext = float(o[10:])*1e3
@ -2006,6 +2057,31 @@ if __name__ == "__main__":
if snakemake.config["sector"]['electricity_distribution_grid']:
insert_electricity_distribution_grid(n)
for o in opts:
if "+" in o:
oo = o.split("+")
carrier_list=np.hstack((n.generators.carrier.unique(), n.links.carrier.unique(),
n.stores.carrier.unique(), n.storage_units.carrier.unique()))
suptechs = map(lambda c: c.split("-", 2)[0], carrier_list)
if oo[0].startswith(tuple(suptechs)):
carrier = oo[0]
attr_lookup = {"p": "p_nom_max", "c": "capital_cost"}
attr = attr_lookup[oo[1][0]]
factor = float(oo[1][1:])
#beware if factor is 0 and p_nom_max is np.inf, 0*np.inf is nan
if carrier == "AC": # lines do not have carrier
n.lines[attr] *= factor
else:
comps = {"Generator", "Link", "StorageUnit"} if attr=='p_nom_max' else {"Generator", "Link", "StorageUnit", "Store"}
for c in n.iterate_components(comps):
if carrier=='solar':
sel = c.df.carrier.str.contains(carrier) & ~c.df.carrier.str.contains("solar rooftop")
else:
sel = c.df.carrier.str.contains(carrier)
c.df.loc[sel,attr] *= factor
print("changing", attr ,"for",carrier,"by factor",factor)
if snakemake.config["sector"]['gas_distribution_grid']:
insert_gas_distribution_costs(n)
if snakemake.config["sector"]['electricity_grid_connection']:

View File

@ -28,10 +28,13 @@ from vresutils.benchmark import memory_logger
override_component_attrs = pypsa.descriptors.Dict({k : v.copy() for k,v in pypsa.components.component_attrs.items()})
override_component_attrs["Link"].loc["bus2"] = ["string",np.nan,np.nan,"2nd bus","Input (optional)"]
override_component_attrs["Link"].loc["bus3"] = ["string",np.nan,np.nan,"3rd bus","Input (optional)"]
override_component_attrs["Link"].loc["bus4"] = ["string",np.nan,np.nan,"4th bus","Input (optional)"]
override_component_attrs["Link"].loc["efficiency2"] = ["static or series","per unit",1.,"2nd bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["efficiency3"] = ["static or series","per unit",1.,"3rd bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["efficiency4"] = ["static or series","per unit",1.,"4th bus efficiency","Input (optional)"]
override_component_attrs["Link"].loc["p2"] = ["series","MW",0.,"2nd bus output","Output"]
override_component_attrs["Link"].loc["p3"] = ["series","MW",0.,"3rd bus output","Output"]
override_component_attrs["Link"].loc["p4"] = ["series","MW",0.,"4th bus output","Output"]
@ -269,6 +272,7 @@ def solve_network(n, config=None, solver_log=None, opts=None):
solver_name=solver_name,
solver_logfile=solver_log,
solver_options=solver_options,
solver_dir=tmpdir,
extra_functionality=extra_functionality,
formulation=solve_opts['formulation'])
#extra_postprocessing=extra_postprocessing